写在前面

在今天如果一个前端还没有听说过VueAngularReact等框架,那基本可以认为这个前端是不及格的。而对于许多前端而言,学会使用Vue和了解Vue背后的实现细节又是完全不一样的事情。如果把写代码比作读书,那么今天的程序员就如中世纪或者古时认字的人一样稀缺,对于没有学过编程的人来说,程序语言是一种魔法或者仙术。然而对于程序员而言,尤其是许多前端程序员,Vue这类框架的实现仍然是一种魔法。仿佛尤雨溪在键盘上挥了挥手施放咒语,就诞生了好用又方便的Vue。

出于自我学习的目的,我决定写一系列文章祛魅(Disenchantment)Vue。Vue的诞生毫无疑问是伟大的,但Vue并不神秘,一切看起来像是魔法的东西都只是因为我们没有仔细地去研究它。至于为什么没有选择Angular和React而单单选择了Vue,一方面当然因为我本人的技术背景,另一方面也是因为我更偏爱没有企业背景的开源产品。

本文的写作除了单纯分析Vue的源代码,也参考了许多同类作品和技术文档。所有的参考资料我会另外写篇文章整理。

要愉快地阅读本文,读者需要具备如下知识:

  1. ES6的语法
  2. flow或TypeScript的语法
  3. 能够使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

拿掉各种处理数据类型和条件判断的部分,这段代码是这样的:

1
2
3
4
5
function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
ob = new Observer(value)
return ob
}

我们看到最后返回了一个Observer对象,而这个Observer构造函数也在这个文件里:

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
class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

同样地,我们也去掉很多条件判断和数据处理的部分(注意这些部分非常重要,但对于本章的目的而言不是重点),可以得到这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Observer {
value: any;
constructor (value: any) {
this.value = value
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}

我们看到Observer里面有一个walk函数,这个函数里面又用到了一个defineReactive函数,defineReactive也位于src/core/observer/index.js中,是这样一个函数:

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
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

代码有点长,但简化之后大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function defineReactive (
obj: Object,
key: string
) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// Do something...
},
set: function reactiveSetter (newVal) {
// Do something...
}
})
}

所谓“千呼万唤始出来,犹抱琵琶半遮面”,经过一层层抽丝剥茧,本章的主角终于出现了,就是Object.defineProperty。通过查询caniuse可以发现,IE8及以前的浏览器直接标为了红色,表明不可用,这就是Vue不能兼容IE8及以前浏览器的真相。

那这是个什么东西呢?我们从MDN的文档里能看到,这是一个定义对象属性的方法,接收三个参数:对象、对象属性名称、对象属性描述。在Vue源码里我们看到,它用于定义的对象属性描述里包含:

1
2
3
4
5
6
7
8
9
10
{
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// Do something...
},
set: function reactiveSetter (newVal) {
// Do something...
}
}

enumerable为true表明对象属性可枚举,configurable为true表明对象属性描述符是可变的。而get和set分别重写了对象属性的get和set方法,除此之外还有一些常用的比如writable,可用于定义对象属性是不是只读的。要知道这个东西怎么用,我们只需要试着在Chrome的控制台里输入这样一段:

1
2
3
4
let o = {a: 'c'}
Object.defineProperty(o, 'a', {
writable: false
})

上面的代码定义了一个对象o,里面有一个键值对{a: ‘c’},然后我们可以写一个赋值语句比如o.a = ‘b’,Chrome会执行这句话,也不会报错,但再次读取o.a的值会发现并没有改变。利用这个特性就可以创造出只读的对象来,在开发中是有意义的。

而要实现数据响应,关键在于set方法。其实这里我们可以大胆猜想,所谓的数据响应,就是当一个数据改变时,通知到所有订阅这个数据的地方,也就是所谓的“发布-订阅”模式。我们试着这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let o = {}
let o_data = 'c'
function notice(obj, key, newVal){
console.log(obj + '下的' + key + '被改成了' + newVal)
}
Object.defineProperty(o, 'a', {
get: function(){
return o_data
},
set: function(x){
notice('o', 'a', x)
o_data = x
}
})

上面的代码里,我们重写了get和set方法,然后我们试图改变o.a的值时,会输出一句被改变的提示。notice函数的作用就是用来通知变动的。实际上我们回头看Vue的源码,会看到它的set最后会执行一句:

1
dep.notify()

这个dep是被import Dep from ‘./dep’这句给引入的,关于dep的作用会在以后的文章中继续探究,但大体上而言,dep的作用是收集所有订阅该数据的订阅者,当值被改变的时候,会最终通知到所有的订阅者。

(第一节 · 完)