热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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等方法。 还是老规矩,我们先看看整体机制图:

MVVM大比拼之avalon.js源码精析

双工引擎

接下来就是直接一探 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 源码分析,敬请期待。


推荐阅读
  • 本文深入探讨了Ajax的工作机制及其在现代Web开发中的应用。Ajax作为一种异步通信技术,改变了传统的客户端与服务器直接交互的模式。通过引入Ajax,客户端与服务器之间的通信变得更加高效和灵活。文章详细分析了Ajax的核心原理,包括XMLHttpRequest对象的使用、数据传输格式(如JSON和XML)以及事件处理机制。此外,还介绍了Ajax在提升用户体验、实现动态页面更新等方面的具体应用,并讨论了其在当前Web开发中的重要性和未来发展趋势。 ... [详细]
  • 在处理木偶评估函数时,我发现可以顺利传递本机对象(如字符串、列表和数字),但每当尝试将JSHandle或ElementHandle作为参数传递时,函数会拒绝接受这些对象。这可能是由于这些句柄对象的特殊性质导致的,建议在使用时进行适当的转换或封装,以确保函数能够正确处理。 ... [详细]
  • Flutter 2.* 路由管理详解
    本文详细介绍了 Flutter 2.* 中的路由管理机制,包括路由的基本概念、MaterialPageRoute 的使用、Navigator 的操作方法、路由传值、命名路由及其注册、路由钩子等。 ... [详细]
  • async/await 是现代 JavaScript 中非常强大的异步编程工具,可以极大地简化异步代码的编写。本文将详细介绍 async 和 await 的用法及其背后的原理。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 本地存储组件实现对IE低版本浏览器的兼容性支持 ... [详细]
  • 2.2 组件间父子通信机制详解
    2.2 组件间父子通信机制详解 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 本文将继续探讨 JavaScript 函数式编程的高级技巧及其实际应用。通过一个具体的寻路算法示例,我们将深入分析如何利用函数式编程的思想解决复杂问题。示例中,节点之间的连线代表路径,连线上的数字表示两点间的距离。我们将详细讲解如何通过递归和高阶函数等技术实现高效的寻路算法。 ... [详细]
  • 2018 HDU 多校联合第五场 G题:Glad You Game(线段树优化解法)
    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6356在《Glad You Game》中,Steve 面临一个复杂的区间操作问题。该题可以通过线段树进行高效优化。具体来说,线段树能够快速处理区间更新和查询操作,从而大大提高了算法的效率。本文详细介绍了线段树的构建和维护方法,并给出了具体的代码实现,帮助读者更好地理解和应用这一数据结构。 ... [详细]
  • 在 Vue 应用开发中,页面状态管理和跨页面数据传递是常见需求。本文将详细介绍 Vue Router 提供的两种有效方式,帮助开发者高效地实现页面间的数据交互与状态同步,同时分享一些最佳实践和注意事项。 ... [详细]
  • 在最近的学习过程中,我对Vue.js中的Prop属性有了更深入的理解,并认为这一知识点至关重要,因此在此记录一些心得体会。Prop属性用于在组件之间传递数据。由于每个组件实例的作用域都是独立的,无法直接引用父组件的数据。通过使用Prop,可以将数据从父组件安全地传递到子组件,确保数据的隔离性和可维护性。 ... [详细]
  • 在探讨 MySQL 正则表达式 REGEXP 的功能与应用之前,我们先通过一个小实验来对比 REGEXP 和 LIKE 的性能。通过具体的代码示例,我们将评估这两种查询方式的效率,以确定 REGEXP 是否值得深入研究。实验结果将为后续的详细解析提供基础。 ... [详细]
  • 本文探讨了如何利用 jQuery 的 JSONP 技术实现跨域调用外部 Web 服务。通过详细解析 JSONP 的工作原理及其在 jQuery 中的应用,本文提供了实用的代码示例和最佳实践,帮助开发者解决跨域请求中的常见问题。 ... [详细]
  • 本文总结了JavaScript的核心知识点和实用技巧,涵盖了变量声明、DOM操作、事件处理等重要方面。例如,通过`event.srcElement`获取触发事件的元素,并使用`alert`显示其HTML结构;利用`innerText`和`innerHTML`属性分别设置和获取文本内容及HTML内容。此外,还介绍了如何在表单中动态生成和操作``元素,以便更好地处理用户输入。这些技巧对于提升前端开发效率和代码质量具有重要意义。 ... [详细]
author-avatar
过客烤翅加盟889
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有