依赖收集
在这一节中,我们来介绍依赖收集,在介绍之前我们需要知道什么是依赖收集,以及依赖收集的目的。
问:什么是依赖收集?依赖收集的目的是什么?
答:依赖收集就是对订阅数据变化的Watcher
收集的过程。其目的是当响应式数据发生变化,触发它们的setter
时,能够知道应该通知哪些订阅者去做相应的逻辑处理。例如,当在template
模板中使用到了某个响应式变量,在组件初次渲染的时候,对这个响应式变量而言,应该收集render watcher
依赖,当其数据发生变化触发setter
时,要通知render watcher
进行组件的重新渲染。
在之前我们提到过,依赖收集发生在Object.defineProperty()
的getter
中,我们回顾一下defineReactive()
代码:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略代码
const dep = new Dep()
// 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
}
})
}
我们可以从代码中看到,当触发getter
的时候,首先判断了Dep.target
是否存在,如果存在则调用dep.depend()
,dep.depend()
函数就是依赖真正收集的地方。在阅读完以上代码后,我们可能会有这样几个疑问:
Dep
是什么?Dep.target
是什么?dep.depend
是如何进行依赖收集的?又是如何进行依赖移除的?
Dep
让我们首先来回答第一个问题,介绍一下Dep
类,Dep
类是定义在observer
目录下dep.js
文件中的一个类,observer
目录结构如下:
|-- observer
| |-- array.js
| |-- dep.js
| |-- index.js
| |-- scheduler.js
| |-- traverse.js
| |-- watcher.js
然后,我们来看一下Dep
类的具体定义:
let uid = 0
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()
}
}
}
代码分析:
Dep
类首先定义了一个静态属性target
,它就是Dep.target
,我们会在之后介绍它。然后又定义了两个实例属性,id
是Dep
的主键,会在实例化的时候自增,subs
是一个存储各种Watcher
的数组。例如render watcher
、user watcher
和computed watcher
等。addSub
和removeSub
对应的就是往subs
数组中添加和移除各种Watcher
。depend
为依赖收集过程。notify
当数据发生变化触发setter
的时候,有一段这样的代码:dep.notify()
,它的目的就是当这个响应式数据发生变化的时候,通知subs
里面的各种watcher
,然后执行其update()
方法。这属于派发更新的过程,我们会在之后的章节介绍。
在介绍完以上几个属性和方法后,我们就对Dep
是什么以及它做哪些事情有了一个具体的认识。
Dep.target和Watcher
我们接下来回答第二个问题,Dep.target
是什么?Dep.target
就是各种Watcher
的实例,以下面代码举例说明:
<tempalte>
<div>{{msg}}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello, Vue.js'
}
}
}
</script>
当组件初次渲染的时候,会获取msg
的值,然后执行pushTarget(this)
,其中this
代表当前Watcher
实例,pushTarget()
函数是定义在dep.js
文件中的一个方法,与之对应的还有一个叫做popTarget
方法,它们的代码如下:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
在pushTarget
中,我们传递的target
参数就是Watcher
实例,然后在pushTarget
执行的时候,它会动态设置Dep
的静态属性Dep.target
的值。在分析完pushTarget
函数的代码后,我们就能明白为什么说Dep.target
就是各种Watcher
的实例了。
然后,我们会存在一个新的问题:Watcher
类是如何定义的?它其实是定义在watcher.js
文件中一个类,其关键代码如下:
let uid = 0
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()
}
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
}
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)
}
}
}
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
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
从依赖收集的角度去看Watcher
类的时候,我们在其构造函数中需要关注以下四个属性:
this.deps = [] // 旧dep列表
this.newDeps = [] // 新dep列表
this.depIds = new Set() // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合
我们会在之后的addDep
和cleanupDeps
环节详细介绍以上四个属性的作用,在这一小节,我们主要关注Watcher
的构造函数以及get()
方法的实现。
在Watcher
类的构造函数中,当实例化时,deps
和newDeps
数组以及depIds
和newDepIds
集合分别被初始化为空数组以及空集合,在构造函数的最后,判断了如果不是computed watcher
(注:只有computed watcher
其lazy
属性才为true
),则会马上调用this.get()
函数进行求值。
接下来,我们来分析一下this.get()
方法的实现,以及pushTarget
和popTarget
方法配合使用的场景介绍。
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
}
我们可以看到,get()
方法的代码不是很复杂,在方法的最前面首先调用pushTarget(this)
,通过pushTarget()
方法首先把当前Watcher
实例压栈到target
栈数组中,然后把Dep.target
设置为当前的Watcher
实例。
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
然后调用this.getter
进行求值,拿以下计算属性示例来说:
export default {
data () {
return {
age: 23
}
},
computed: {
newAge () {
return this.age + 1
}
}
}
value = this.getter.call(vm, vm)
// 相当于
value = newAge()
对于computed watcher
而言,它的getter
属性就是我们撰写的计算属性方法,调用this.getter
的过程,就是执行我们撰写的计算属性方法进行求值的过程。
在this.get()
方法的最后,调用了popTarget()
,它会把当前target
栈数组的最后一个移除,然后把Dep.target
设置为倒数第二个。
Dep.target = null
const targetStack = []
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
在分析了pushTarget
和popTarget
后,我们可能会有一个疑问,就是为什么会存在这样的压栈/出栈的操作,这样做的目的是什么?
这样做的目的是因为组件是可以嵌套的,使用栈数组进行压栈/出栈的操作是为了在组件渲染的过程中,保持正确的依赖,以下面代码为例:
// child component
export default {
name: 'ChildComponent',
template: '<div>{{childMsg}}</div>',
data () {
return {
childMsg: 'child msg'
}
}
}
export default {
name: 'ParentComponent',
template: `<div>
{{parentMsg}}
<child-component />
</div>`,
components: {
ChildComponent
}
data () {
return {
parentMsg: 'parent msg'
}
}
}
我们都知道,组件渲染的时候,当父组件中有子组件时,会先渲染子组件,子组件全部渲染完毕后,父组件才算渲染完毕,因此组件渲染钩子函数的执行顺序为:
parent beforeMount()
child beforeMount()
child mounted()
parent mounted()
根据以上渲染步骤,当parent beforeMount()
开始执行时,会进行parent render watcher
实例化,然后调用this.get()
,此时的Dep.target
依赖为parent render watcher
,target
栈数组为:
// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher']
当child beforeMount
开始执行的时候,会进行child render watcher
实例化,然后调用this.get()
,此时的Dep.target
依赖为child render watcher
,target
栈数组为:
// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher', 'child render watcher']
当child mounted()
执行时,代表子组件的this.getter()
调用完毕,进而会调用popTarget()
进行出栈操作,此时的栈数组和Dep.target
会发生变化:
// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher']
Dep.target = 'parent render watcher'
当parent mounted()
执行时,代表父组件的this.getter()
调用完毕,进而会调用popTarget()
进行出栈操作,此时的栈数组和Dep.target
会发生变化:
// 演示使用,实际为Watcher实例
const targetStack = []
Dep.target = undefined
通过以上示例分析,我们就弄明白了为什么会有依赖压栈/出栈这样的步骤以及这样做的目的了。
接下来,让我们来分析依赖收集的过程中,addDep
和cleanupDeps
的逻辑。
addDep和cleanupDeps
addDep
在之前Dep
类的depend()
方法中,我们介绍过其代码实现,它会调用addDep(dep)
:
export default Dep {
// 省略其它代码
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
根据前面的分析内容,我们知道Dep.target
其实就是各种Watcher
实例,因此Dep.target.addDep(this)
相当于:
const watcher = new Watcher()
watcher.addDep(this)
接下来,让我们来看一下Watcher
类中,addDep
方法的实现逻辑:
export default Watcher {
// 精简代码
constructor () {
this.deps = [] // 旧dep列表
this.newDeps = [] // 新dep列表
this.depIds = new Set() // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合
}
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)
}
}
}
}
addDep
方法的逻辑不是很复杂,首先判断了当前dep
是否已经在新dep id
集合中,不在则更新新dep id
集合以及新dep
数组,随后又判断了当前dep
是否在旧dep id
集合中,不在则调用dep.addSub(this)
方法,把当前Watcher
实例添加到dep
实例的subs
数组中。
生硬的分析源码不是很方便我们理解addDep
的代码逻辑,我们以下面代码示例说明:
<template>
<p>位置一:{{msg}}</p>
<p>位置二:{{msg}}</p>
</template>
<script>
export default {
name: 'App',
data () {
return {
msg: 'msg'
}
}
}
</script>
过程分析:
- 当组件初次渲染的时候,会实例化
render watcher
,此时的Dep.target
为render watcher
:
const updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 第一次编译读取
msg
响应式变量时,触发getter
进行dep.depend()
依赖收集,然后调用addDep()
方法,因为deps
、newDeps
、depIds
和newDepIds
初始化为空数组或者空集合,所以此时的dep
被添加到newDepIds
、newDeps
中并且会执行dep.addSub(this)
,此时可以用下面代码表示:
// 实例化Dep
const dep = {
id: 1,
subs: []
}
// 添加到newDepIds,newDeps
this.newDepIds.push(1)
this.newDeps.push(dep)
// 调用addSub
dep.addSub(this)
console.log(dep) // { id: 1, subs: [new Watcher()] }
- 当第二次编译读取
msg
响应式变量时,触发getter
进行dep.depend
依赖收集,因为dep
是defineReactive
函数中的闭包变量,因此两次触发的getter
是同一个dep
实例。当调用addDep
判断此时的newDepIds
集合中dep.id
为1
已经存在,因此直接跳过。
你可能会发现,在分析getter
中代码的时候,我们故意忽略了下面这段代码:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
你可能会有这样的疑问:这点代码是干什么的?有什么作用?那么现在,我们举例说明:
<template>
<p>{{obj.msg}}</p>
</template>
<script>
export default {
name: 'App',
data () {
return {
obj: {
msg: 'msg'
}
}
}
}
</script>
过程分析:
- 当第一次调用
defineReactive
时,此时defineReactive
第一个参数obj
和key
分别为:
obj = {
obj: {
msg: 'msg'
}
}
key = 'obj'
在defineReactive
在最开始,实例化了一个闭包dep
实例,我们假设实例化后的dep
如下:
const dep = new Dep()
console.log(dep) // { id: 1, subs: [] }
当代码执行到observe(val)
的时候,根据之前我们分析过observe
代码的逻辑,因为参数obj[key]
的值是一个普通对象,因此会执行new Observer()
实例化,而在Observer
构造函数中,有这样一段代码:
this.dep = new Dep()
它又实例化了一个dep
并且把实例化后的dep
赋值给this.dep
,我们假设此时实例化后的dep
如下所示:
const dep = new Dep()
console.log(dep) // { id: 2, subs: [] }
因为obj = { msg: 'msg' }
是一个对象,因此执行this.walk()
遍历obj
对象的属性,然后再次调用defineReactive
又实例化了一个闭包dep
实例,我们假设实例后的dep
如下所示:
const dep = new Dep()
console.log(dep) // { id: 3, subs: [] }
现在,我们已经有了三个dep
实例了,其中两个是defineReactive
函数中的闭包实例dep
,另外一个是childOb
(Observer
实例)的属性dep
。
- 在组件开始渲染的时候,根据响应式原理加上我们在
template
中读取了obj.msg
变量,因此会先触发obj
对象的getter
,此时dep
为id=1
的那个闭包变量dep
。此时的Dep.target
为render watcher
,然后进行dep.depend()
依赖收集,当走到addDep
方法的时候,因为我们关注的四个属性全部为空数组或者空集合,因此会把此时的dep
添加进去,此时的dep
表示如下:
const dep = {
id: 1,
subs: [new Watcher()]
}
- 在
dep.depend()
依赖收集完毕后,会判断childOb
,因为childOb
为Observer
的实例,因此条件判断为真,调用childOb.dep.depend()
。当执行到addDep()
时,此时的dep
为id=2
的那个Observer
实例属性dep
,不在newDepIds
和depIds
中,因此会把其添加进去,此时的dep
表示如下:
const dep = {
id: 2,
subs: [new Watcher()]
}
- 当响应式变量
obj
的getter
触发完毕后,会触发obj.msg
的getter
,此时的dep
为id=3
的那个闭包变量dep
。此时的Dep.target
依然为render watcher
,然后进行dep.depend()
依赖收集,这个过程与obj
的getter
进行依赖收集的过程基本是一样的,当addDep()
方法执行后,此时的dep
表示如下:
const dep = {
id: 3,
subs: [new Watcher()]
}
唯一的区别时,此时的childOb
为undefined
,不会调用childOb.dep.depend()
进行子属性的依赖收集。
在分析完以上代码后,我们很容易回答以下问题:
问:childOb.dep.depend()
是干什么的?有什么作用?
答:childOb.dep.depend()
这段代码是进行子属性的依赖收集,这样做的目的是为了当对象或者对象属性任意一个发生变化时,都可以通知其依赖进行相应的处理。
<template>
<p>{{obj.msg}}</p>
<button @click="change">修改属性</button>
<button @click="add">添加属性</button>
</template>
<script>
import Vue from 'vue'
export default {
name: 'App',
data () {
return {
obj: {
msg: 'msg'
}
}
},
methods: {
change () {
this.obj.msg = 'new msg'
},
add () {
this.$set(this.obj, 'age', 23)
}
},
watch: {
obj: {
handler () {
console.log(this.obj)
},
deep: true
}
}
}
</script>
拿以上例子说明:
- 当存在
childOb.dep.depend()
收集子属性依赖时,我们无论是修改msg
的值还是添加age
新属性,都会触发user watcher
,也就是打印this.obj
的值。 - 当不存在
childOb.dep.depend()
收集子属性依赖时,我们修改msg
的值,虽然会通知render watcher
进行组件重新渲染,但不会通知user watcher
打印this.obj
的值。
cleanupDeps
在这一小节,我们的目标是弄清楚为什么要进行依赖清除以及如何进行依赖清除。
先来看Watcher
类中对于cleanupDeps
的实现:
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
}
}
我们还是举例说明,假如有如下组件:
<template>
<p v-if="count < 1">{{msg}}</p>
<p v-else>{{age}}</p>
<button @click="change">Add</button>
</template>
<script>
import Vue from 'vue'
export default {
name: 'App',
data () {
return {
count: 0,
msg: 'msg',
age: 23
}
},
methods: {
change () {
this.count++
}
}
}
</script>
过程分析:
- 当组件初次渲染完毕后,
render watcher
实例的newDeps
数组有两个dep
实例,其中一个是在count
响应式变量getter
被触发时收集的,另外一个是在msg
响应式变量getter
被触发时收集的(age
因为v-if/v-else
指令的原因,在组件初次渲染的时候不会触发age
的getter
),我们使用如下代码进行表示:
this.deps = []
this.newDeps = [
{ id: 1, subs: [new Watcher()] },
{ id: 2, subs: [new Watcher()] }
]
- 当我们点击按钮进行
this.count++
的时候,会触发组件重新更新,因为count < 1
条件为假,因此在组件重新渲染的过程中,也会触发age
响应式变量的getter
进行依赖收集。当执行完addDep
后,此时newDeps
发生了变化:
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])
在最后一次调用this.get()
的时候,会调用this.cleanupDeps()
方法,在这个方法中首先遍历旧依赖列表deps
,如果发现其中某个dep
不在新依赖id
集合newDepIds
中,则调用dep.removeSub(this)
移除依赖。在组件渲染的过程中,this
代表render watcher
,调用这个方法后当我们再修改msg
变量值的时候,就不会触发组件重新渲染了。在遍历完deps
数组后,会把deps
和newDeps
、depIds
和newDepIds
的值进行交换,然后清空newDeps
和newDepIds
。
在分析完以上示例后,我们就能明白为什么要进行依赖清除了:避免无关的依赖进行组件的重复渲染、watch回调等。