vue.js源码精析

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

VUE 源码分析

简介

Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google。vue 如作者自己所说,在api设计上受到了很多来自knockout、angularjs等大牌框架影响,但作者相信 vue 在性能、易用性方面是有优势。同时也自己做了和其它框架的性能对比,在这里
今天以版本 0.10.4 为准

入口

Vue 的入口也很直白:

1
var demo = new Vue({ el: '#demo', data: { message: 'Hello Vue.js!' } })

  

和 ko 、avalon 不同的是,vue 在一开始就必须指定 el 。个人认为这里设计得不是很合理,因为如果一份数据要绑定到两个不同dom节点上,那就不得不指定一个同时包含了这两个dom节点的祖先dom节点。
接下来去找 Vue 的定义。翻开源码,vue 用 grunt。build命令中用了作者自己写的gulp-component来组合代码片段。具体请读者自己看看,这里不仔细说了。

从 /src/main.js 里看到,Vue 的定义就是 ViewModal 的定义。打开 ViewModel,发现它的定义中只是实例化了一个 Compiler,把自己作为参数传给构造函数。同时看到 ViewModel 原型上定义了一些方法,基本上是跟内部事件、dom 操作有关。那接下来我们就主要看看这个 compiler了。不要忘了我们第一个目的是找到它双工绑定的主要原理。

双工绑定

翻到 compiler 的定义,代码太长。犹豫了一下决定还是删掉一些注释贴出来,因为基本上大部分值得看的都在这里,愿深入的读者最好看源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
function Compiler (vm, options) {
    var compiler = this,
        key, i
 
    compiler.init       = true
    compiler.destroyed  = false
    options = compiler.options = options || {}
    utils.processOptions(options)
    extend(compiler, options.compilerOptions)
    compiler.repeat   = compiler.repeat || false
    compiler.expCache = compiler.expCache || {}
    var el = compiler.el = compiler.setupElement(options)
    utils.log('\nnew VM instance: ' + el.tagName + '\n')
     
    compiler.vm       = el.vue_vm = vm
    compiler.bindings = utils.hash()
    compiler.dirs     = []
    compiler.deferred = []
    compiler.computed = []
    compiler.children = []
    compiler.emitter  = new Emitter(vm)
 
    if (options.methods) {
        for (key in options.methods) {
            compiler.createBinding(key)
        }
    }
 
    if (options.computed) {
        for (key in options.computed) {
            compiler.createBinding(key)
        }
    }
 
    // VM ---------------------------------------------------------------------
 
    vm.$         = {}
    vm.$el       = el
    vm.$options  = options
    vm.$compiler = compiler
    vm.$event    = null
 
    var parentVM = options.parent
    if (parentVM) {
        compiler.parent = parentVM.$compiler
        parentVM.$compiler.children.push(compiler)
        vm.$parent = parentVM
    }
    vm.$root = getRoot(compiler).vm
 
    // DATA -------------------------------------------------------------------
    compiler.setupObserver()
     
    var data = compiler.data = options.data || {},
        defaultData = options.defaultData
    if (defaultData) {
        for (key in defaultData) {
            if (!hasOwn.call(data, key)) {
                data[key] = defaultData[key]
            }
        }
    }
 
    var params = options.paramAttributes
    if (params) {
        i = params.length
        while (i--) {
            data[params[i]] = utils.checkNumber(
                compiler.eval(
                    el.getAttribute(params[i])
                )
            )
        }
    }
 
    extend(vm, data)
    vm.$data = data
    compiler.execHook('created')
    data = compiler.data = vm.$data
 
    var vmProp
    for (key in vm) {
        vmProp = vm[key]
        if (
            key.charAt(0) !== '$' &&
            data[key] !== vmProp &&
            typeof vmProp !== 'function'
        ) {
            data[key] = vmProp
        }
    }
 
    compiler.observeData(data)
 
    // COMPILE ----------------------------------------------------------------
    if (options.template) {
        this.resolveContent()
    }
    while (i--) {
        compiler.bindDirective(compiler.deferred[i])
    }
    compiler.deferred = null
 
    if (this.computed.length) {
        DepsParser.parse(this.computed)
    }
 
    compiler.init = false
    compiler.execHook('ready')
}

  

注释就已经写明了 compiler 实例化分为四个阶段,第一阶段是一些基础的设置。两个值得注意的点:一是在 compiler 里面定义一个 vm 属性来保存对传入的 ViewModel 的引用;二是对 method 和 computed 的每一个成员都调用了 createBinding 。跳到 createBinding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CompilerProto.createBinding = function (key, directive) {
    /*省略*/
    var compiler = this,
        methods  = compiler.options.methods,
        isExp    = directive && directive.isExp,
        isFn     = (directive && directive.isFn) || (methods && methods[key]),
        bindings = compiler.bindings,
        computed = compiler.options.computed,
        binding  = new Binding(compiler, key, isExp, isFn)
 
    if (isExp) {
        /*省略*/
    } else if (isFn) {
        bindings[key] = binding
        binding.value = compiler.vm[key] = methods[key]
    } else {
        bindings[key] = binding
        if (binding.root) {
            /*省略*/
            if (computed && computed[key]) {
                // computed property
                compiler.defineComputed(key, binding, computed[key])
            } else if (key.charAt(0) !== '$') {
                /*省略*/
            } else {
                /*省略*/
            }
        } else if (computed && computed[utils.baseKey(key)]) {
            /*省略*/
        } else {
            /*省略*/
        }
    }
    return binding
}

  

它做了两件事情:一是实例化了一个叫做 Bingding 的东西,二是将 method 和 computed 成员的 bingding 进行了一些再处理。凭直觉和之前看过的代码,我们可以大胆猜测这个实例化的 bingding 很可能就是用来保存数据和相应地"更新回调函数"的集合。点进 /src/binding 里。果然,看到其中的 update 、pub 等函数和 sub 、dir 等对象成员,基本证明猜对了。

到这里,实例化的对象已经有点多了。后面还会更多,为了让各位不迷失,请提前看看这张关键对象图:

vue.js源码精析

看完 bingding,我们继续回到 createBinding 中,刚才还说到对 method 和 computed 成员的 bingding 做了一些再处理。对 method,就直接在 vm 上增加了一个同名的引用,我们可以把 vm 看做一个公开的载体,在上面做引用就相当于把自己公开了。对 computed 的成员,使用defineComputed 做的处理是:在vm上定义同名属性,并将 getter/setter 对应到相应computed成员的$get和$set。

至此,compiler 的第一部分做完,基本上把数据的架子都搭好了。我们看到 bingding 的 pub 和 sub, 知道了 vue 也是就与 observe 模式,那接下来就看看它是如何把把视图编译成数据更新函数,并注册到bingding里。

回到compiler里,第二部分处理了一下vm,增加了一些引用。 第三部分关键的来了,一看就知道最重要的就是第一句 compiler.setupObserver() 和最后一句compiler.observeData(data) 。直接看源码的读者,注释里已经很清楚了。第一句是用来注册一些内部事件的。最后一句是用来将数据的成员转化成 getter/setter。并和刚刚提到的bingding 相互绑定。值得注意的是,如果遇到数据成员是对象或者数组,vue 是递归式将它们转化成 getter/setter 的,所以你嵌套多深都没关系,直接替换掉这些成员也没关系,它对新替换的对象重新递归式转化。

这里的代码都很易懂,读者可以自己点进去看。我只想说一点,就是 vue 在内部实现中使用了很多事件派发器,也就是 /src/emitter。比如对数据的 set 操作。在 set 函数只是触发一个 set 事件,后面的视图更新函数什么都是注册这个事件下的。这个小小的设计让关键的几个模块解耦得非常好,能够比较独立地进行测试。同时也为框架本身的扩展提供了很多很多的空间。下面这张图展示了对data的成员进行修改时内部的事件派发:

vue.js源码精析

视图渲染和扩展

看到最后一部分视图渲染,这里值得注意的是,vue 支持的是 angular 风格的可复用的directive。directive 的具体实现和之前的 ko 什么的没太大区别,都是声明 bind、update等函数。

至于扩展方面,vue已有明确的 component 和 plugin 的概念,很好理解,读者看看文档即可。 另外注意下,vue 是到最后才处理 computed 和普通数据的依赖关系的。

总结

总体来说,vue 在内核架构上很精巧。精指的是没有像ko一样先实现一些强大但复杂的数据结构,而是需要什么就实现什么。巧指的是在代码架构上既完整实现了功能,又尽量地解耦,为扩展提供了很大的空间。比如它使用了 binding 这样一个中间体,而不是将试图更新函数直接注册到数据的set函数中等等,这些设计都是值得学习了。 当然我们也看到了一些有异议的地方: 比如是否考虑将数据的转化和视图编译明确分成两个过程?这样容易实现数据的复用,也就是最开始讲的问题。这样改的话,compiler 的实例化的代码也可以稍微更优雅一些:先处理数据和依赖关系,再建立bingding并绑定各种事件,最后处理视图。

这几天有事去了, 没按时更新,抱歉。下一期带来angular源码分析,敬请期待。

 

技术

 
摘要: VUE 源码分析简介Vue是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google。vue 如作者自己所说,在api设计上受到了很多来自knockout、angularjs等大牌框架影响,但作者相信 vue 在性能、易用性方面是有优势。同时也自己做了和其它框架的性能对比,在这...阅读全文
posted @ 2014-04-27 18:35 侯振宇 阅读(386) | 评论 (0) 编辑
 
摘要: 简介本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档。knockout.js(以下简称ko)是最早将 MVVM 引入到前端的重要功臣之一。目前版本已更新到 3 。相比同类主要有特点有:双工绑定基于 observe 模式,性能高。插件和扩展机制非常完善,无论在数据层还是展现层都能满足各种...阅读全文
posted @ 2014-04-21 00:48 侯振宇 阅读(1244) | 评论 (7) 编辑
 
摘要: 这个最佳实践是我目前人在做的一个站点,主要功能:oauth登录发布文章(我称为"片段"),片段可以自定义一些和内容有关的指标,如“文中人物:12”。支持自定义排版、插图、建立相册。多个片段可以组织在一起,形成"事件"。任意事件、片段都可以互相标记因果关系。可以follow事件或者他人。事件和片段模糊搜索。我对目前前端框架的观点已在另一篇文章中讲过,这里只介绍一下目前实践的情况。使用requirejs做模块化,上线时用r.js打包。使用avalon做数据与视图渲染框架。用page.js来管理路由。用harp来做静态资源服务器。harp支持less、coff阅读全文
posted @ 2013-12-30 18:08 侯振宇 阅读(4154) | 评论 (26) 编辑
 
摘要: 打给比方,如果说angularJS是剑,那么avalon就是匕首。匕首比剑更易学,更快,更适合快速作战...我们还是直接来实际的吧:目前项目开发中有以下几个通用需求:有前端路由系统,用来划分逻辑模块。有异步模块载入,如requireJS或者seaJS。数据与视图自动绑定,并且性能出众。框架学习曲线不要太陡,并且让我的代码越少越好。路由AngularJS不能无缝与第三方路由整合,只能用它自己的模块,如”ui-status“。原因是AngularJS的数据模型是包裹在它的“controller”作用域里面的。当然你可以把这些数据模型抛到全局作用域里,但即便如此,你对这些数据模型操作以后还是要手动调阅读全文
posted @ 2013-08-14 22:47 侯振宇 阅读(2302) | 评论 (5) 编辑
 
摘要: 有复杂的地方我再开贴记录,这里只记录容易解决的坑。1. windows下手工增加smart package。直接将下载下来的包扔到meteor package中。记得将文件夹名字改得和smart.js文件中一样。2. meteor不能实现在document中存“DBref”。所以有时候要用类“多表连接”的实现。github上有现成的smart package。https://github.com/erundook/meteor-publish-with-relations3. 使用nodejs的收信和解析包时,node-mail和mailparser优点坑,看这里http://www.cnbl阅读全文
posted @ 2013-07-19 11:00 侯振宇 阅读(1001) | 评论 (0) 编辑
 
摘要: 版本信息:Meteor:windows MIS安装 0.6.4node-imap:npm指定的0.8.0版,不是默认的0.7.x版。mailparser:npm安装0.3.6以下是记录踩到的坑:1. 使用meteor的email来发送邮件时,设置process.env.MAIL_URL要注意,如果你的gmail账号是自己设置了domain的,如“xxx@unitedstack.com”。那么process.env.MAIL_URL的写法应该是:“smtp://xxx%40unitedstack.com:YOUR_PASSWORD@smtp.googlemail.com:465”。一定注意第一个阅读全文
posted @ 2013-07-19 10:44 侯振宇 阅读(746) | 评论 (0) 编辑
 
摘要: 标题党了。其实这篇文章是为了记录一个我对AngularJS compile过程的一个问题。基础:directive有个重要特性是transclude。这个特性主要用途是:当页面的dom元素要进行复杂变化时,将原本的dom暴露给用户以供变形时或变形后使用。例如我有个选项卡directive,写法是:<div tabs> <div tab title="title1">content1</div> <div tab title="title2">content2</div></div>这阅读全文
posted @ 2013-05-15 15:54 侯振宇 阅读(1163) | 评论 (1) 编辑
 
摘要: 我在数据层的抽象上走了一段比较长的弯路。简单总结一下:刚开始做简单的项目时,用简单的ActiveRecord就已经很合适了,比如CodeIgniter自带的。但是项目变大之后,特别是业务实体之间的关联变多之后马上就出现了问题,为了降低各个实体间的耦合,你需要将各种操作再抽象出一层。举个例子:你做了一个简单的用户系统,对用户的增删改查只需要对“用户类”调用相应的操作就行了。后来你增加了一个文件类,每个用户可以拥有多个文件,删除用户时需要同时删除相应的文件。为了能使删除的用户的这个操作复用,同时防止其他操作删除用户时没有删除相应文件夹,于是你不得不再对用户的删除做一层封装。我曾经试图让各种实体在构阅读全文
posted @ 2013-03-12 16:18 侯振宇 阅读(868) | 评论 (0) 编辑
 
摘要: 在学习Django模板之前我以为也和很多php模板引擎一样,是通过缓存文件输出,执行模板中的python代码来实现的。读完发现居然和前端模板相似,用了正则。为了更好的体会设计的哲学,我同时参考了tornato、drupal、codeigniter、aceTemplate、underscore中的模板设计。Django模板特点1.不支持python代码,使用自己的标签关键字。 Django的模板设计时有个重要的假设是:“写模板的不懂python”。因此你只要搞告诉他基本的逻辑结构(判断、循环、输出变量等)的写法就行了。同时也避免了程序员将业务逻辑写道模板里。解析模板的时候和前端模板非常类似,也.阅读全文
posted @ 2013-03-08 15:45 侯振宇 阅读(832) | 评论 (0) 编辑
 
摘要: 这篇是总目录。记录下学习python的过程是为了强制自己执行新的学习方法,以及验证自己对于“学习”本身的新领悟。真正强大的人在于成长有个加速度。这个加速度体现在对自己思维方式的改造上,远不只是经验积累。这个改造的方式来自于日常学习中。普通人学习的误区在与于只是收获解决某类问题的经验,即使你学的是一种思路。对真正的学习来说,这只是第一步,我称之为通览。第二步是研究所学的东西的形成过程和发展趋势,对于其中疑惑的地方单独拿出来再学习,这一步才是学习的本质。第三步是在所学的东西上发挥灵感,再创造。这个想法得益于之前看到的一篇文章《如何在一年内修完斯坦福四年的课程》。就是我学习的详细记录:计划1 学习p阅读全文
posted @ 2013-03-08 10:22 侯振宇 阅读(160) | 评论 (1) 编辑
 
摘要: climbPHP是一个基于CodeIginter的框架,下面记录的是我开发的整个思路。【初衷】我终于决定好好写一篇东西来总结一下climbPHP,之前总是觉得没开发完,还不是时候。现在觉得可能永远都是处于没开发完的阶段。最早写climbPHP时的动因很简单,就是想把前端常用的事件类和模块间通过事件类来通信这种模式模式移植到后端来。说白了就是一个全局的观察者模式。写着写着我发现自己的思路得到了新的启发,主要有两点:第一,将系统的任何操作都看作是对外界刺激的反射的话(看《失控》学到的东西),使用事件的方式比起抵用接口来说更容易理解,也更容易表述业务逻辑。第二,引导模块使用事件来通信能它们能更加松耦阅读全文
posted @ 2012-11-23 20:41 侯振宇 阅读(887) | 评论 (3) 编辑
 

最新评论

  • 1. Re:MVVM大比拼之avalon.js源码精析
  • @eflay
    抱歉最近比较忙,刚更新。
  • --侯振宇
  • 2. Re:MVVM大比拼之avalon.js源码精析
  • 已经大后天了啊。。
  • --eflay
  • 3. Re:MVVM大比拼之knockout.js源码精析
  • @横渡除非你是一直在做玩具,性能的上的要求怎么可能比较少触及??我说的200行表格渲染只是个例子,指出的是当angular在处理数组和嵌套对象时的diff机制有问题。现在随便做个单页应用页面的viewModel对象就上百,如果前端还要通过数据缓存来减少页面请求的话,数据何止200?你在3楼说angu......
  • --侯振宇
  • 4. Re:MVVM大比拼之knockout.js源码精析
  • @侯振宇事实上,对于性能上的要求,一般来说比较少可以触及,比如你说的200行以上的表格渲染。基本这是特殊情况而不分页了。姑且不说angularjs是致力于做单页应用出生的,就knockout而言,只因它的出生是MS,我个人都对它先提保守之心。回观历史,MS在web领域上是真的没有给业界带来什么,同时......
  • --横渡
  • 5. Re:MVVM大比拼之knockout.js源码精析
  • @横渡
    有没有支持异步的,是那种无阻塞等待的异步,因为需要执行顺序但不想要阻塞界面,不是回调异步。
  • --eflay
 
 
Copyright ©2014 侯振宇
分类: 技术学习
上一篇:MVVM大比拼之knockout.js源码精析


下一篇:CSS实现垂直居中的4种思路