通过断点跟踪探索Vue响应原理
基于版本2.5.2源码分析
响应原理
- Object.defineProperty()
- 观察者模式
响应原理的探索
文章对于Vue原理的探索基于下面的测试工程,结合浏览器断点跟踪堆栈的方式阅读源码。
测试工程代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<template>
<div id="app">
{{ num }}
<button @click="click">+1</button>
</div>
</template>
<script>
export default {
name: 'app',
data () {
return {
num: 1
}
},
methods: {
click () {
this.num += 1
}
}
}
</script>
|
运行效果
Vue对观察者模式的实现
核心三个类:Observer、Dep、Watcher
开始时,需要通过Observer类将数据转换成可被观察的对象。
Observer类的构造方法里通过this.walk()和this.observeArray()方法,间接地调用了Object.defineProperty(),Object.defineProperty()参数中的getter方法会使用一个new Dep()的闭包实例对象,该实例主要用于依赖收集(即收集Watcher实例)。
所以每个可被观察的对象都与一个Dep类实例绑定在一起,一对一关系
例如Vue会对this.num转换成一个可被观察的对象,将一个闭包的Dep实例注入到this.num的getter方法里。当this.num被读取值时,并且有Watcher实例,就会收集依赖,将该Watcher实例与闭包的Dep实例相互引用。这样,只要当this.num的setter方法被调用时,就可通过闭包的Dep实例通知所有的Watcher实例执行一个回调。
Dep类实例和Watcher实例关系是多对多
这三个类之间的通信过程如下:
- 将数据A通过Observer类转换成一个可被观察的对象。
- 改写数据A的getter和setter方法,并在getter方法里,嵌入一个闭包对象Dep类实例。
- 对数据A感兴趣的数据B创建一个Watcher类实例,把数据A的读取方式(可能function或字符串)当成参数传进Watcher类。
- Watcher类构造实例时,先将自己放到全局的Dep.target上,然后读取数据A,触发数据A的getter方法。
- 数据A getter方法里检查到Dep.target有值,收集依赖Watcher实例。
- 那以后数据A只要有值被更新,即调用setter方法,就能通知到所有依赖的Watcher实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Observer 类
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
// Observer的构造器
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) // 内部调用defineReactive方法
}
}
}
|
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
|
// defineReactive 方法
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 闭包的Dep类实例
const dep = new Dep()
// 省略部分代码
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dep.target 不为null,即有Watcher实例
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()
}
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
16
17
18
19
20
21
22
23
24
25
|
// Dep 类
export default class Dep {
target: ?Watcher;
id: number;
subs: Array<Watcher>;
// 省略部分代码
// 与Watcher实例互相引用
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 调用所有Watcher的update方法,触发Watcher的回调
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
|
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
|
// Watcher 类
export default class Watcher {
// 省略部分代码
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
// 省略部分代码
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
// 每次new Watcher()时调用此方法
get () {
// 将自身的Watcher实例赋值到全局的Dep.target,等待下面调用this.getter.call()时,如果触发数据getter方法,就可以自动收集依赖
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
// 此方法会被Dep类实例触发,与其互相引用
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 省略部分代码
}
|
断点调试的方式跟踪源码
在源码中按Object.defineProperty()搜索,可以定位到主要的函数。根据响应原理,使用defineProperty()的方法一定会设置getter和setter方法
- src/core/observer/index.js的defineReactive()
- src/core/instance/state.js的defineComputed()
先在defineReactive()方法里的getter方法设置断点,命中后,可看到getter方法的返回值符合预期,是初始化值1。并且代码运行至936行,进行依赖收集,监听num=1的变化。
跟踪堆栈的内容,可以发现是Vue将template块转换成render函数后,在读取_vm.num值时触发到getter方法的。
前提,需要先对vm.$data下所有的字段调用defineProperty()才能触发依赖的收集。同样通过断点,发现这个过程是通过调用src/core/instance/state.js的initData方法
如果对num的值进行更新,就会相应触发setter方法。并在setter方法的最后,会通过dep.notify()通知所有依赖num值的依赖方。
computed属性如何被监听的?
- 先加一个computed属性
1
2
3
4
5
6
|
computed: {
plusNum () {
return this.num + 1
}
}
|
- 在template里使用plusNum
1
2
3
4
5
6
7
|
<template>
<div id="app">
{{ num }}
{{ plusNum }}
<button @click="click">+1</button>
</div>
</template>
|
最后在defineComputed()和createComputedGetter()这两个方法里加个断点。
computed里的字段通过initComputed()自动初始化,初始化时,在defineComputed()方法里调用仍然是通过defineProperty()进行设置getter和setter方法。只是computed属性字段比data特殊,setter方法不需要设置。
通常computed属性字段会依赖vm.$data的属性字段。例如this.plusNum就依赖了this.num。
Vue做法是this.num值被更新时(触发this.num的setter方法里的dep.notify()),会通过Watcher类的update方法将dirty值设置为True。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
export default class Watcher {
update () {
/* istanbul ignore else */
if (this.lazy) {
// computed属性字段在初始化Watcher实例时会将lazy赋值为True
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
|
每次this.plusNum被取值时(触发getter方法),会检查watcher.dirty,True的话,表明依赖有更新,需要重新计算。
首先对测试工程代码进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<template>
<div id="app">
<!-->删掉{{ num }}的引用</-->
<button @click="click">+1</button>
</div>
</template>
<script>
export default {
data () {
return {
num: 1,
plusNum: 0 // 新增属性plusNum
}
},
watch: {
// 新增num的watch方法
num (newVal) {
this.plusNum = this.num + 1
}
}
}
</script>
|
Vue对watch功能的实现思路是在初始化方法initWatch()中,调用了实例方法的vue.$watch,$watch里会new一个Watcher用于监听,在Watcher的构造方法里,通过调用this.get()会触发到this.num的getter(),getter()里会把这儿new出来的Watcher实例当成依赖收集起来。只要this.num的setter()被触发,就会通过dep.notify()触发到Watcher实例的回调方法。
大概就是这样子,watch功能就实现了。这个过程通过堆栈可看出来。
总结
- vm.$data里的数据通过initData()更新,把每个数据转换成可观察对象。
- vm.computed里的数据通过defineComputed()方法重写getter方法,getter方法内部进行依赖收集,依赖更新时,不实时更新vm.computed,而是设置Watcher实例的dirty字段,只有在真正读取vm.computed值时,才进行重新计算。
- vm.watch里的数据调用vm.$watch,收集依赖,依赖更新值时,触发watch数据定义的方法。