原文地址:https://banggan.github.io/2019/01/19/Vue源码解读之依赖收集/
通过上一篇Vue源码解读之响应式原理,我们可以看到Vue利用defineReactive会把普通的对象装换为响应式对象,在defineReactive函数中,重点是const dep = new Dep(),在设置getter的时候,收集依赖也就是dep.depend()
依赖收集
先看一下getter做的事情:访问对象属性的时候触发
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()//在每个响应式键值的闭包中定义一个dep对象
..........
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//拿到getter,没有getter就直接取val
const value = getter ? getter.call(obj) : val
//依赖收集的过程 Dep类:建立数据和watch之间的桥梁
if (Dep.target) {
dep.depend() // 进行收集依赖
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value //最终返回value
},
// ...
})
}
在getter 的时候进行依赖的收集,只有在 Dep.target 中有值的时候才会进行依赖收集;
这个 Dep.target 是在Watcher实例的 get 方法调用的时候 pushTarget 会把当前取值的watcher推入 Dep.target,原先的watcher压栈到 targetStack 栈中,当前取值的watcher取值结束后出栈并把原先的watcher值赋给 Dep.target,cleanupDeps 最后把新的 newDeps 里已经没有的watcher清空,以防止视图上已经不需要的无用watcher触发
Dep
Dep 是整个 getter 依赖收集的核心,主要是建立数据和watcher之间的桥梁,主要是记录哪些Watcher依赖自己的变化,dep.depend()作用是在调用Dep.target的addDep()函数。
let uid = 0 、、Dep的实例ID,为了方便去重
export default class Dep {
static target: ?Watcher;// 当前是谁在进行依赖收集
id: number;
subs: Array<Watcher>; //所有的watcher集合
constructor () {
this.id = uid++ //Dep的实例id
this.subs = [] //存储收集器中需要通知的watcher
}
addSub(sub: Watcher) { ... } //添加一个观察者对象
removeSub(sub: Watcher) { ... } //移除一个观察者对象
depend () {//依赖收集,当存在Dep.target的时候把自己添加观察者的依赖中
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() { ... } // 通知所有订阅者
}
}
Dep.target = null
const targetStack = [] // watcher栈
//将watcher观察者实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
//将观察者实例从target栈中取出并设置给Dep.target
export function popTarget () {
Dep.target = targetStack.pop()
}
Dep 是一个 Class,它定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。它是Watcher的实例,用来通知更新。
watcher
再来看一下watcher的实现:
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean //是否是渲染watcher的标志位
) {
this.getter = expOrFn // 在get方法中执行
if (this.computed) { // 是否是 计算属性
this.value = undefined
this.dep = new Dep()//计算属性创建过程中并未求值
} else { // 不是计算属性会立刻求值
this.value = this.get()
}
}
/* 获得getter的值并且重新进行依赖收集 */
get() {
pushTarget(this) // 设置Dep.target = this
let value
value = this.getter.call(vm, vm)
popTarget() // 将观察者实例从target栈中取出并设置给Dep.target
this.cleanupDeps()
return value
}
addDep(dep: Dep)//添加一个依赖关系到Deps集合中
cleanupDeps() // 清理newDeps里没有的无用watcher依赖
update()// 调度者接口,当依赖发生改变的时候进行回调
run() // 调度者工作接口,将被调度者回调
getAndInvoke(cb: Function) { ... }
evaluate() //收集该watcher的所有deps依赖
depend() // 收集该watcher的所有deps依赖,只有计算属性使用
teardown() // 将自身从所有依赖收集订阅列表删除
}
依赖收集流程
当我们实例化一个渲染watcher的时候,执行this.get方法。进行get函数执行pushTarget(),实际上把Dep.target 赋值为当前的渲染 watcher 并压栈,接着执行value = this.getter.call(vm, vm),实际上调用getter方法,触发数据的getter,从而调用dep.depend方法,也就会执行Dep.target.addDep(this):
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)
}
}
}
此时会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs。