# 依赖收集
首先我要知道,什么是依赖,以及收集依赖的目的是什么,即我们究竟要收集谁,收集了之后可以做什么。
收集谁?换句话说就是当属性发生变化时,我们需要通知谁。
我们需要通知到使用数据的地方,它可以是个模板,也可以是用户自定义的 watch
等多个地方和不同的类型,因此需要我们封装一个可以集中处理这些情况的类。然后,我们在收集依赖阶段只收集这个封装好的类的实例进来,当数据发生变化时,也只通知它一个,然后由它负责通知其它地方。在 Vue
中这个类就是 Watcher
。
之前我们也说过在 getter
中收集依赖,在 setter
中派发更新,因此我们来回顾下收集依赖的代码:
export 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()
}
})
}
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
这里我们可以看到首先执行了这串代码 const dep = new Dep()
,然后在定义的 getter
函数中先判断了 Dep.target
存在后,则调用 dep.depend()
函数来进行依赖收集,同时还对 childOb
进行判断及后续处理,因此我们首先需要高清以下几个问题:
- 什么是
Dep
? Dep.target
又是什么?dep.depend()
方法内部是如何实现依赖收集的
# Dep
首先让我们来搞懂什么是 Dep
,它是定义在 observe
目录中的 dep.js
文件中。其代码如下所示:
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
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
从上面我们可以知道,Dep
类实际上是专门用来管理 Watcher
(依赖)的类,其首先定义了一个静态属性 target
,其值是全局唯一的 Watcher
,因为同一时间只有一个 Watcher
会被计算,然后其自身属性 id
相当于 Dep
的逐渐,会在实例化的时候自增。同时自身属性 subs
是存储各种 Watcher
的数组,如 render Watcher
、user Watcher
和 computed Watcher
等。
Dep
的其它方法就是添加和移除各种各样的 Watcher
,以及执行 Watcher
中的方法,因此我们来看 Watcher
即依赖究竟是怎样实现的。
# Watcher
Watcher
是定义在 observe/watcher.js
文件中的类,其代码如下所示:
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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.
*/
get () {
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.
*/
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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
从依赖收集角度来讲,我们在构造函数中主要需要关注以下四个属性:
this.deps = [] // 旧dep列表
this.newDeps = [] // 新dep列表
this.depIds = new Set() // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合
2
3
4
这里使用的 Set
集合,是在判断当前环境如果不支持原生方法,则使用自己实现的方法,其定义在 core/util/env.js
中:
let _Set
/* istanbul ignore if */ // $flow-disable-line
if (typeof Set !== 'undefined' && isNative(Set)) {
// use native Set when available.
_Set = Set
} else {
// a non-standard Set polyfill that only works with primitive keys.
_Set = class Set implements SimpleSet {
set: Object;
constructor () {
this.set = Object.create(null)
}
has (key: string | number) {
return this.set[key] === true
}
add (key: string | number) {
this.set[key] = true
}
clear () {
this.set = Object.create(null)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们在实例化 Watcher
的时候,会初始化一系列属性,其中其中重要的一点是获取 getter
函数,通过调用该函数就可以获取需要监听的数值。我们可以通过一下经典的使用方式来理解:
vm.$watch('a.b.c', function(newVal, oldVal){
// do something
})
2
3
当调用上述方法 api
方法的时候,在构造函数中,会进行如下赋值:
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这是当监听的如果是一个表达式的时候,则通过 parsePath
函数读取字符串的 keyPath
来获取监听数据的值的,其定义在 core/util/lang.js
中:
/**
* Parse simple path.
*/
const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里并不复杂,先讲 keyPath
分割成数组,然后循环数组一层一层去读取数据,从而最后返回的就是想要的数据。
在构造函数最后,判断如果不是 computed Watcher
(只有 computed Watcher
的 lazy
属性才为 true
),那么会立即调用 this.get()
函数,接下来我们就分析 this.get()
方法的实现。
get () {
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们可以看到,首先调用 pustTarget(this)
方法,该方法定义在 dep.js
文件中
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
2
3
4
5
6
7
8
9
10
该方法的作用主要是将当前 Watcher
实例添加到 targetStack
中,然后把 Dep.target
设置为当前的 Watcher
实例。
所以 Dep.target
实际上就是一个 Watcher
实例。
然后会调用 this.getter
函数来进行求值。
value = this.getter.call(vm, vm)
我们这里拿计算属性来作为示例:
export default {
data () {
return {
count: 1
}
},
computed: {
counter () {
return this.count + 1
}
}
}
// value = this.getter.call(vm, vm)
// 相当于
value = counter()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当我们调用方法时,就会调用对象属性的数据进行访问,从而触发数据对象上的 getter
,每个 getter
属性上都有一个 dep
, 在触发 getter 的时候,这时存在当前唯一的 Dep.target
, 从而会调用 dep.depend()
来收集依赖,即会执行以下方法:
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
2
3
4
5
# addDep 和 cleanupDeps
由之前调用的 pushTarget
可知,Dep.target
已经被赋值为当前 Watcher
,从而可以执行 addDep
方法:
/**
* Add a dependency to this directive.
*/
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)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
addDep
方法首先判断了当前 dep
是否已经在新 dep ids
集合中,不在则更新 dep ids
集合以及新 dep
数组,随后又判断了当前 dep
是否在旧 dep id
集合中,不在则说明没有将当前 Watcher
订阅到当前数据负责依赖管理 dep
中的 subs
(用于后续派发更新), 因此会调用 dep.addSub(this)
方法,把当前 Watcher
实例添加到 subs
数组中。
# addDep 运行示例说明
为了加深对 addDep
的理解,我们通过以下示例说明
<template>
<p>first:{{msg}}</p>
<p>second:{{msg}}</p>
</template>
<script>
export default {
name: 'App',
data () {
return {
msg: 'hello world'
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
当初次渲染该组件的时候,会实例化
render Watcher
,此时Dep.target
上的Watcher
实例就是render Watcher
updateComponent = () => { vm._update(vm._render(), hydrating) } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
1
2
3
4
5
6
7
8
9
10
11
12
13
14当第一次读取页面的
msg
变量时,会触发getter
从而调用dep.depend
方法进行依赖收集,在该方法内部调用addDep
方法,这时deps
、depIds
、newDeps
和newDepIds
被初始化为空数组或者空的集合。当执行完addDep
方法后,会将当前的dep
添加到newDeps
数组中,其id
被添加到newDepIds
中,最后会讲当前Watcher
实例添加到dep
的订阅数组subs
中。// 实例化 Dep const dep = { id: 0, subs: [] } // 第一次执行 addDep 之后 // 此时的 Watcher 实例 const watcher = { newDeps: [dep], newDepIds: [0], } dep = { id: 0, subs: [watcher] }
1
2
3
4
5
6
7
8
9
10当第二次读取
msg
变量的时候,会再次出发getter
进行依赖收集,由于实例化的dep
对于getter
是定义在defineReactive
函数中的闭包变量,因此两个触发的getter
中是同一个dep
实例,此时调用addDep
方法,会判断newDepIds
集合中已经存在dep.id
为1
的,因此会直接跳过依赖收集。
# popTarget
依赖收集完成之后,会执行以下几个逻辑,首先是:
if (this.deep) {
traverse(value)
}
2
3
这时当我们设置了 deep
属性的时候,会通过调用 traverse(value)
方法,递归来触发所有子项的 getter
,从而对每个嵌套的属性都会进行依赖收集,这个将在后面介绍 $watcher
API 的时候详细说明。
然后会执行 popTarget()
方法,其定义在 core/observe/dep.js
文件中,如下:
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
2
3
4
这里实际上将 targetStack
出栈,然后将 Dep.target
设置为最后一项,即恢复到上一个状态,因为当前的数据依赖已经收集完成,对应的 Dep.target
也需变化。那么我们为什么要进行这样的操作呢?
这样的目的主要是为了保持正常的依赖关系,如以下代码:
当渲染组件时,会先渲染子组件,当子组件渲染完成之后,父组件才能渲染完毕,其渲染时的钩子函数如下:
parent beforeMount()
child beforeMount()
child mounted()
parent mounted()
2
3
4
当 parent beforeMount
执行时,会执行父组件的 render watcher
进行实例化,然后调用 this.get()
方法,这时的 Dep.target
为父组件的 render watcher
。targetStack
的伪代码如下:
// 伪代码,实际为 Watcher 实例
Dep.target = 'parent render watcher'
targetStack = ['parent render watcher']
2
3
当子组件的 beforeMount
开始执行时,会对子组件的 render watcher
进行实例化,然后调用 this.get()
方法,这时的 Dep.target
为子组件的 render Watcher
。其伪代码如下所示:
// 伪代码,实际为 Watcher 实例
Dep.target = 'child render watcher'
targetStack = ['parent render watcher', 'child render watcher']
2
3
当执行子组件的 mounted
方法时,代表 this.getter()
调用完毕,之后会执行 popTarget
方法,执行出栈操作,此时栈数组和 Dep.target
都会发生变化
// 伪代码,实际为 Watcher 实例
Dep.target = 'parent render watcher'
targetStack = ['parent render watcher']
2
3
这样就保证了父子组件正确的依赖关系。
# cleanupDeps
最后我们会执行 this.cleanupDeps()
方法,这里主要是进行了依赖清空操作,那么我们为什么要进行清空依赖呢?具体又是怎样清空依赖的?
首先我们可以看到 Watcher
类中其代码实现如下:
export default Watcher {
// 精简代码
constructor () {
this.deps = [] // 旧dep列表
this.newDeps = [] // 新dep列表
this.depIds = new Set() // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
}
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
# cleanupDeps 示例
为了更好的说明其作用,我们举例来说明,假如我们有以下组件:
<template>
<div id="app">
<p v-if="count < 1">{{msg}}</p>
<p v-else>{{age}}</p>
<button @click="changeCount">Add Count</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: 'hello world',
age: 12,
count: 0
};
},
methods: {
changeCount() {
this.count++
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
当组件初次进行渲染的时候,render Watcher
实例上面的 newDeps
数组里面有两个 dep
实例,分别是 msg
和 count
属性触发 getter
的时候被收集,这里由于 v-if/v-else
的原因,并不会触发 age
属性的 getter
。执行 addDep
之后的伪代码如下所示:
// 伪代码
this.deps = []
this.depIds = new Set()
this.newDepIds = new Set([1,3])
this.newDeps = [
{ id: 1, subs: [new Watcher()] },
{ id: 2, subs: [new Watcher()] }
]
2
3
4
5
6
7
8
当我们点击 Add Count
之后,会执行 this.count++
,触发 count
的 setter
函数,从而会触发组件更新,由于此时 count < 1
判断不再成立,因此会触发 age
的 getter
,在进行依赖收集 addDep
之后,会发生如下变化:
// 伪代码
this.deps = [
{ id: 1, subs: [new Watcher()] },
{ id: 2, subs: [new Watcher()] }
]
this.newDeps = [
{ id: 1, subs: [new Watcher()] },
{ id: 3, subs: [new Watcher()] }
]
this.depIds = new Set([1, 2])
this.newDepIds = new Set([1, 3])
2
3
4
5
6
7
8
9
10
11
在调用 this.get()
函数的最后会执行 cleanupDeps
函数,这个方法首先会遍历 this.deps
旧的依赖列表,当发现其中的某个 dep.id
不存在于 newDepIds
集合中,那么就会调用 dep.removeSub(this)
方法将依赖进行移除,现在这里的 this
代表 render watcher
。调用完这个方法之后,后续我们再次修改 msg
属性的值的时候就不会在触发组件渲染了。之后会将 deps
与 newDeps
以及 depIds
与 newDepIds
的值进行更换,然后清空 newDeps
和 newDepIds
的值,如下最终伪代码:
this.depIds = new Set([1, 3])
this.deps = [
{ id: 1, subs: [new Watcher()] },
{ id: 3, subs: [new Watcher()] }
]
this.newDeps = []
this.newDepIds = new Set()
2
3
4
5
6
7