写在前面
在今天如果一个前端还没有听说过Vue、Angular、React等框架,那基本可以认为这个前端是不及格的。而对于许多前端而言,学会使用Vue和了解Vue背后的实现细节又是完全不一样的事情。如果把写代码比作读书,那么今天的程序员就如中世纪或者古时认字的人一样稀缺,对于没有学过编程的人来说,程序语言是一种魔法或者仙术。然而对于程序员而言,尤其是许多前端程序员,Vue这类框架的实现仍然是一种魔法。仿佛尤雨溪在键盘上挥了挥手施放咒语,就诞生了好用又方便的Vue。
出于自我学习的目的,我决定写一系列文章祛魅(Disenchantment)Vue。Vue的诞生毫无疑问是伟大的,但Vue并不神秘,一切看起来像是魔法的东西都只是因为我们没有仔细地去研究它。至于为什么没有选择Angular和React而单单选择了Vue,一方面当然因为我本人的技术背景,另一方面也是因为我更偏爱没有企业背景的开源产品。
本文的写作除了单纯分析Vue的源代码,也参考了许多同类作品和技术文档。所有的参考资料我会另外写篇文章整理。
要愉快地阅读本文,读者需要具备如下知识:
- ES6的语法
- flow或TypeScript的语法
- 能够使用Vue
另外我本人水平也有限,难免会犯错误。这系列文章与其说是写给社区的,不如说是写给我自己的技术笔记,所以在严谨程度上可能有些欠缺,也不会随着Vue的版本更新而更新,我会尽力做到细致,但可能整体而言还是粗线条的,毕竟Vue本身就是一个颇为庞大的工程。如果文章中有错误,欢迎指正。
那么这就开始吧。

一 · 并不神奇的响应式
有没有想过为什么在Vue的技术文档里要提到自己并不兼容IE8及以前的浏览器?文档里只是用一句“it uses ECMAScript 5 features that are un-shimmable in IE8”糊弄过去了,并不是因为Vue偷懒所以不想兼容到IE8及以前的古代浏览器(虽然就算偷懒也没有什么大问题……),而是因为数据响应这个Vue最重要的特性之一,其实现用到了IE8无法支持的ES5特性。
具体来讲,翻阅Vue源码,在src/core/observer/index.js处我们可以看到这样一段代码:
1 | function observe (value: any, asRootData: ?boolean): Observer | void { |
拿掉各种处理数据类型和条件判断的部分,这段代码是这样的:
1 | function observe (value: any, asRootData: ?boolean): Observer | void { |
我们看到最后返回了一个Observer对象,而这个Observer构造函数也在这个文件里:
1 | class Observer { |
同样地,我们也去掉很多条件判断和数据处理的部分(注意这些部分非常重要,但对于本章的目的而言不是重点),可以得到这个:
1 | class Observer { |
我们看到Observer里面有一个walk函数,这个函数里面又用到了一个defineReactive函数,defineReactive也位于src/core/observer/index.js中,是这样一个函数:
1 | function defineReactive ( |
代码有点长,但简化之后大概是这样的:
1 | function defineReactive ( |
所谓“千呼万唤始出来,犹抱琵琶半遮面”,经过一层层抽丝剥茧,本章的主角终于出现了,就是Object.defineProperty。通过查询caniuse可以发现,IE8及以前的浏览器直接标为了红色,表明不可用,这就是Vue不能兼容IE8及以前浏览器的真相。
那这是个什么东西呢?我们从MDN的文档里能看到,这是一个定义对象属性的方法,接收三个参数:对象、对象属性名称、对象属性描述。在Vue源码里我们看到,它用于定义的对象属性描述里包含:
1 | { |
enumerable为true表明对象属性可枚举,configurable为true表明对象属性描述符是可变的。而get和set分别重写了对象属性的get和set方法,除此之外还有一些常用的比如writable,可用于定义对象属性是不是只读的。要知道这个东西怎么用,我们只需要试着在Chrome的控制台里输入这样一段:
1 | let o = {a: 'c'} |
上面的代码定义了一个对象o,里面有一个键值对{a: ‘c’},然后我们可以写一个赋值语句比如o.a = ‘b’,Chrome会执行这句话,也不会报错,但再次读取o.a的值会发现并没有改变。利用这个特性就可以创造出只读的对象来,在开发中是有意义的。
而要实现数据响应,关键在于set方法。其实这里我们可以大胆猜想,所谓的数据响应,就是当一个数据改变时,通知到所有订阅这个数据的地方,也就是所谓的“发布-订阅”模式。我们试着这样写:
1 | let o = {} |
上面的代码里,我们重写了get和set方法,然后我们试图改变o.a的值时,会输出一句被改变的提示。notice函数的作用就是用来通知变动的。实际上我们回头看Vue的源码,会看到它的set最后会执行一句:
1 | dep.notify() |
这个dep是被import Dep from ‘./dep’这句给引入的,关于dep的作用会在以后的文章中继续探究,但大体上而言,dep的作用是收集所有订阅该数据的订阅者,当值被改变的时候,会最终通知到所有的订阅者。
(第一节 · 完)