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 等对象成员,基本证明猜对了。
到这里,实例化的对象已经有点多了。后面还会更多,为了让各位不迷失,请提前看看这张关键对象图:

看完 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 支持的是 angular 风格的可复用的directive。directive 的具体实现和之前的 ko 什么的没太大区别,都是声明 bind、update等函数。
至于扩展方面,vue已有明确的 component 和 plugin 的概念,很好理解,读者看看文档即可。 另外注意下,vue 是到最后才处理 computed 和普通数据的依赖关系的。
总结
总体来说,vue 在内核架构上很精巧。精指的是没有像ko一样先实现一些强大但复杂的数据结构,而是需要什么就实现什么。巧指的是在代码架构上既完整实现了功能,又尽量地解耦,为扩展提供了很大的空间。比如它使用了 binding 这样一个中间体,而不是将试图更新函数直接注册到数据的set函数中等等,这些设计都是值得学习了。 当然我们也看到了一些有异议的地方: 比如是否考虑将数据的转化和视图编译明确分成两个过程?这样容易实现数据的复用,也就是最开始讲的问题。这样改的话,compiler 的实例化的代码也可以稍微更优雅一些:先处理数据和依赖关系,再建立bingding并绑定各种事件,最后处理视图。
这几天有事去了, 没按时更新,抱歉。下一期带来angular源码分析,敬请期待。
技术
最新评论
- 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
阅读排行榜
评论排行榜
vue.js源码精析的更多相关文章
- MVVM大比拼之vue.js源码精析
VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularj ...
- MVVM大比拼之knockout.js源码精析
简介 本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档. knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一.目前版本已更新到 3 .相比同类主要有 ...
- MVVM大比拼之avalon.js源码精析
简介 avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是: 使用 observe 模式,性能高. 将原始对象用object.defineProperty重写,不需要用户像用knoc ...
- MVVM大比拼之AngularJS源码精析
MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...
- Vue.js源码——事件机制
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- 从Vue.js源码角度再看数据绑定
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- vue源码分析—Vue.js 源码构建
Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...
- 2018-11-23 手工翻译Vue.js源码:尝试重命名标识符与文本
续前文: 手工翻译Vue.js源码第一步:14个文件重命名 对core/instance/索引中的变量, 方法进行重命名如下(题图): import { 混入初始化 } from './初始化' im ...
随机推荐
- ISA TEST Writeup
刚出来的hack游戏,非常easy,现在只有7关.考虑入门级.没有什么可以玩. http://helloisa.com/ LEVEL 1 细致观察页面,入侵的第一步是收集一切可能产生价值的信息 ps: ...
- HTTP必知必会(转)
HTTP协议作为网络传输的基本协议,有着广泛的应用.HTTP协议的完整内容很多,但是其核心知识却又简单精炼.学习者应该掌握其基本结构,并且能够举一反三.这篇文章所列的,就是在实际开发中必须知道必须掌握 ...
- Topshelf创建Windows服务
使用Topshelf创建Windows服务 概述 Topshelf是创建Windows服务的另一种方法,老外的一篇文章Create a .NET Windows Service in 5 steps ...
- javascript、jQuery的扩展方法,扩展实例展示代码
$(function () { var total = 0, height = $(window).height(), memberScroll, cartScroll, proScroll; ...
- Bag标签之中的一个行代码实行中文分词实例1
例1: 分词(返回以逗号隔开的词组,gap=",") <bagid=pPage act=2words name=words gap=",">我喜欢黄 ...
- 【百度地图API】如何区分地址解析和智能搜索?
原文:[百度地图API]如何区分地址解析和智能搜索? 摘要: 很多用户一直无法区分地址解析geocoder和智能搜索localsearch的使用场景.该文章用一个详尽的示例,充分展示了这两个类,共5种 ...
- Linux下javaweb
Linux下javaweb环境搭建 步骤: 1.使用远程工具连接上服务器,例如xsheel(ssh).filezilla(ftp) 2.JDK安装及相关配置 3.Mysql安装及相关配置 4.Tomc ...
- Windbg Extension NetExt
Windbg Extension NetExt 摘要 : 在使用WINDBG做debugging的时候,需要一个好的工具帮助进行数据分析. 最常见的extension包括SOS, PSSCOR. N ...
- asp.net 发邮件
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.We ...
- 如何将经纬度利用Google Map API显示C# VS2005 Sample Code
原文 如何将经纬度利用Google Map API显示C# VS2005 Sample Code 日前写了一篇如何用GPS抓取目前所在,并回传至资料库储存,这篇将会利用这些回报的资料,将它显示在地图上 ...