zoukankan      html  css  js  c++  java
  • MVVM大比拼之avalon.js源码精析

    简介

    avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是:

    • 使用 observe 模式,性能高。
    • 将原始对象用object.defineProperty重写,不需要用户像用knockout时那样显示定义各种属性。
    • 对低版本的IE使用了VBScript来兼容,一直兼容到IE6。

    需要看基础介绍的话建议直接看司徒的博客。在网上搜了一圈,发现已经有了avalon很好的源码分析,这里也推荐一下:地址。 avalon在圈子里一直被诟病不够规范的问题,请各位不必再留言在我这里,看源码无非是取其精华去其糟粕。可以点评,但总是讨论用哪个框架好哪个不好就没什么意义了,若是自己把握不住,用什么都不好。

    今天的分析以 avalon.mobile 1.2.5 为准,avalon.mobile 是专门为高级浏览器准备的,不兼容IE8以下。

    入口

    还是先看启动代码

    avalon.ready(function() {
        avalon.define("box", function(vm) {
            vm.w = 100;
            vm.h = 100;
            vm.area = function(){
            	get : function(){ return this.w * this.h }
            }
            vm.logW =function(){ console.log(vm.w)}
            
        })
        avalon.scan()
    })
    

      

    还是两件事:定义viewModel 和 执行扫描。 翻到define 定义:

    avalon.define = function(id, factory) {
            if (VMODELS[id]) {
                log("warning: " + id + " 已经存在于avalon.vmodels中")
            }
            var scope = {
                $watch: noop
            }
            factory(scope) //得到所有定义
            var model = modelFactory(scope) //偷天换日,将scope换为model
            stopRepeatAssign = true
            factory(model)
            stopRepeatAssign = false
            model.$id = id
            return VMODELS[id] = model
        }
    

      

    其实已经可以一眼看明白了。这里只提一点,为什么要执行两次factory?建议读者先自己想一下。我这里直接说出来了: 因为modelFactory中,如果属性是函数,就会被直接复制到新的model上,但函数内的vm却仍然指向的原来的定义函数的中的vm,因此发生错误。所以通过二次执行factory来修正引用错误。
    那为什么不在modelFactory中直接就把通过Function.bind或其他方法来把引用给指定好呢?而且可以在通过scope获得定以后就直接把 scope 对象修改成viewModel就好了啊?
    这里的代码写法其实是直接从avalon兼容IE的完整版中搬出来的,因为对老浏览器要创造VBScript对象,所以只能先传个scope进去获取定义,在根据定义去创造。并且老的浏览器也不支持bind等方法。 还是老规矩,我们先看看整体机制图:

    双工引擎

    接下来就是直接一探 modelFactory 内部了。翻到代码 324 行。

    function modelFactory(scope, model) {
            if (Array.isArray(scope)) {
                var arr = scope.concat()//原数组的作为新生成的监控数组的$model而存在
                scope.length = 0
                var collection = Collection(scope)
                collection.push.apply(collection, arr)
                return collection
            }
            if (typeof scope.nodeType === "number") {
                return scope
            }
            var vmodel = {} //要返回的对象
            model = model || {} //放置$model上的属性
            var accessingProperties = {} //监控属性
            var normalProperties = {} //普通属性
            var computedProperties = [] //计算属性
            var watchProperties = arguments[2] || {} //强制要监听的属性
            var skipArray = scope.$skipArray //要忽略监控的属性
            for (var i = 0, name; name = skipProperties[i++]; ) {
                delete scope[name]
                normalProperties[name] = true
            }
            if (Array.isArray(skipArray)) {
                for (var i = 0, name; name = skipArray[i++]; ) {
                    normalProperties[name] = true
                }
            }
            for (var i in scope) {
                loopModel(i, scope[i], model, normalProperties, accessingProperties, computedProperties, watchProperties)
            }
            vmodel = Object.defineProperties(vmodel, descriptorFactory(accessingProperties)) //生成一个空的ViewModel
            for (var name in normalProperties) {
                vmodel[name] = normalProperties[name]
            }
            watchProperties.vmodel = vmodel
            vmodel.$model = model
            vmodel.$events = {}
            vmodel.$id = generateID()
            vmodel.$accessors = accessingProperties
            vmodel[subscribers] = []
            for (var i in Observable) {
                vmodel[i] = Observable[i]
            }
            Object.defineProperty(vmodel, "hasOwnProperty", {
                value: function(name) {
                    return name in vmodel.$model
                },
                writable: false,
                enumerable: false,
                configurable: true
            })
            for (var i = 0, fn; fn = computedProperties[i++]; ) { //最后强逼计算属性 计算自己的值
                Registry[expose] = fn
                fn()
                collectSubscribers(fn)
                delete Registry[expose]
            }
            return vmodel
        }
    

      

    前面声明了一对变量作为容器,用来保存转换过的 控制属性(相当于ko中的observable) 和 计算属性(相当于ko中的computed) 等等。往下翻到最关键的352行,这个 loopModel 函数就是用来生成好各个属性的入口了。继续深入:

    function loopModel(name, val, model, normalProperties, accessingProperties, computedProperties, watchProperties) {
            model[name] = val
            if (normalProperties[name] || (val && val.nodeType)) { //如果是元素节点或在全局的skipProperties里或在当前的$skipArray里
                return normalProperties[name] = val
            }
            if (name[0] === "$" && !watchProperties[name]) { //如果是$开头,并且不在watchProperties里
                return normalProperties[name] = val
            }
            var valueType = getType(val)
            if (valueType === "function") { //如果是函数,也不用监控
                return normalProperties[name] = val
            }
            var accessor, oldArgs
            if (valueType === "object" && typeof val.get === "function" && Object.keys(val).length <= 2) {
                var setter = val.set,
                        getter = val.get
                accessor = function(newValue) { //创建计算属性,因变量,基本上由其他监控属性触发其改变
                    var vmodel = watchProperties.vmodel
                    var value = model[name],
                            preValue = value
    
                  if (arguments.length) {
                        if (stopRepeatAssign) {
                            return
                        }
    
                        if (typeof setter === "function") {
                            var backup = vmodel.$events[name]
                            vmodel.$events[name] = [] //清空回调,防止内部冒泡而触发多次$fire
                            setter.call(vmodel, newValue)
                            vmodel.$events[name] = backup
                        }
                        if (!isEqual(oldArgs, newValue)) {
                            oldArgs = newValue
                            newValue = model[name] = getter.call(vmodel)//同步$model
                            withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循环绑定中的代理VM
    
                          notifySubscribers(accessor) //通知顶层改变
                            safeFire(vmodel, name, newValue, preValue)//触发$watch回调
                        }
                    } else {
                        if (avalon.openComputedCollect) { // 收集视图刷新函数
                            collectSubscribers(accessor)
                        }
                        newValue = model[name] = getter.call(vmodel)
                        if (!isEqual(value, newValue)) {
                            oldArgs = void 0
                            safeFire(vmodel, name, newValue, preValue)
                        }
                        return newValue
                    }
                }
                computedProperties.push(accessor)
            } else if (rchecktype.test(valueType)) {
                accessor = function(newValue) { //子ViewModel或监控数组
                    var realAccessor = accessor.$vmodel, preValue = realAccessor.$model
                    if (arguments.length) {
                        if (stopRepeatAssign) {
                            return
                        }
    
                      if (!isEqual(preValue, newValue)) {
    
                          newValue = accessor.$vmodel = updateVModel(realAccessor, newValue, valueType)
                            var fn = rebindings[newValue.$id]
                            fn && fn()//更新视图
                            var parent = watchProperties.vmodel
                            withProxyCount && updateWithProxy(parent.$id, name, newValue)//同步循环绑定中的代理VM
                            model[name] = newValue.$model//同步$model
                            notifySubscribers(realAccessor)   //通知顶层改变
                            safeFire(parent, name, model[name], preValue)   //触发$watch回调
                        }
                    } else {
                        collectSubscribers(realAccessor) //收集视图函数
                        return realAccessor
                    }
                }
                accessor.$vmodel = val.$model ? val : modelFactory(val, val)
                model[name] = accessor.$vmodel.$model
            } else {
                accessor = function(newValue) { //简单的数据类型
                    var preValue = model[name]
                    if (arguments.length) {
                        if (!isEqual(preValue, newValue)) {
                            model[name] = newValue //同步$model
                            var vmodel = watchProperties.vmodel
                            withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循环绑定中的代理VM
                            notifySubscribers(accessor) //通知顶层改变
                            safeFire(vmodel, name, newValue, preValue)//触发$watch回调
                        }
                    } else {
                        collectSubscribers(accessor) //收集视图函数
                        return preValue
                    }
                }
                model[name] = val
            }
            accessor[subscribers] = [] //订阅者数组
            accessingProperties[name] = accessor
        }
    

      

    源码的注释其实已经写得非常清楚了,如果你看过我上一篇对knockout源码的解读,你会发现avalon这里面的机制和knockout几乎是一样的。函数无非就是根据定义函数中各个属性的类型来生成读写器(accessor),这个读写器会用在后面的 defineProperty 中。这里唯一值得提一下的就是那个 updateWithProxy 函数。只有一种情况需要用到它,就是当页面上使用了 ms-repeat 或者其他循环绑定来处理 数组或对象 时,会生为循环中的对象生成一个代理对象,这个代理对象记录除数据本身外和作用于相关的一些变量,和knockout的bindingContext有些像。 好了,到这里源码基本上没什么难度,我们来做一点有意思的事情。还记得之前我们提出的关于 执行两次 factory的 疑问吗?第二次执行主要是为了修正函数属性中的引用,我们看上面这代码中,但属性的类型是function时,就直接复制,如果我们对这个函数执行一下bind的方法呢,是不是就不用使用factory修正引用了?来试一下,先将 318 行的二次执行factory注释掉。再loopModel函数中 424 行改成

                

    return normalProperties[name] = val.bind(model)
    

      

    我们写个页面载入改过的avalon,然后跑一下这段测试:

    var vma = avalon.define('a',function(vm){
    
        vm.a = "a"
    
        vm.b = "b"
    
        vm.c = {
    
            get : function(){return this.a+this.b}
    
        }
    
        vm.c2 = {
    
            get : function(){return vm.a+vm.b}
    
        }
    
        vm.d = function(){
    
            return this.a+this.b //注意这里用的是 this
    
        }
    
    })
    
    vma.a = "c"
    
    console.log(vma.c == vma.a+vma.b)
    
    console.log(vma.d() == vma.a+vma.b)
    

      

    有没有验证,结果大家最好自己试验一下。 这里可以看到,如果只是针对现代浏览器,avalon的内核还是有很多可以重构的地方的。

    viewModel的内部实现已经搞清,接下来就只剩看看如何处理和页面元素的绑定了。翻到 1214 行scan函数的定义,主要是执行了 scanTag 。再看,主要是执行了 scanAttr。再看,终于找到了和 knockout 看起来一样的 bindingHandlers 了,再往下翻翻就会发现和 knockout 是一样的绑定机制了。读者可以自己看,看不懂的地方翻翻我上一篇中ko的同样部分看看就知道了。

    其他

    最后还是讲讲对数组的处理。之前在ko中我们看到ko为对象专门准备了一个observableArray,里面重写pop等方法,以保证在处理函数时能只通知改动元素相关的绑定,而不用修改整个数组绑定的视图。在avalon中,我们看到在 loopModel 467行的 rchecktype.test(valueType) 这个语句。rchecktype 是个正则 /^(?:object|array)$/ ,也就是判断该属性是不是对象或数组。如果是,在 491 行 的

    accessor.$vmodel = val.$model ? val : modelFactory(val, val)

    又生成一个modelFactory,这时传入modelFactory的第一个参数就可能是数组了,再看modelFacotry 定义,当第一个函数为数组时,将其变成了一个Collection对象,而Collection也是重写了各种数组方法。果然,机制大家都差不多。不过司徒在博客中强调了它的数组处理效率更高,大家可以自己看看。

    最后推荐两篇作者的博客文章,看看他在写MVVM中更多技术细节

    迷你MVVM框架 avalonjs 实现上的几个难点
    迷你MVVM框架avalon在兼容旧式IE做的努力

    还是那句话,取其精华。明天将带来MVVM新贵 vue.js 源码分析,敬请期待。

  • 相关阅读:
    CODING x 百果园 _ 水果零售龙头迈出 DevOps 体系建设第一步
    Nocalhost 亮相 CD Foundation 国内首届 Meetup,Keith Chan 将出席致辞
    做云原生时代标准化工具,实现高效云上研发工作流
    打造数字化软件工厂 —— 一站式 DevOps 平台全景解读
    WePack —— 助力企业渐进式 DevOps 转型
    CODING Compass —— 打造行云流水般的软件工厂
    Nocalhost —— 让云原生开发回归原始而又简单
    CODING 代码资产安全系列之 —— 构建全链路安全能力,守护代码资产安全
    Nocalhost:云原生开发新体验
    使用 Nocalhost 开发 Kubernetes 中的 APISIX Ingress Controller
  • 原文地址:https://www.cnblogs.com/sskyy/p/3679572.html
Copyright © 2011-2022 走看看