话不多说,先上图
首先,Vue的响应式系统本质上就是一个发布-订阅模式,在响应式数据使用时收集watcher,在响应式数据变更时统一对watcher进行处理。
什么是dep
dependency,负责收集watcher。每一个响应式数据(例如ref、computed)在初始化时都维护着一个Dep对象。比如当一个响应式数据ref(count)在watch和模板字符串中使用时,这个响应式数据维护的Dep就需要收集当前这个watch函数和渲染函数,以便在响应式数据变化时重新执行这两个函数。
什么是sub
Subcriber,一个用于管理上面所说watcher的对象,每个watcher对应一个Subcriber对象。例如watch函数、渲染函数、computed函数都会创建一个Subcriber。其中Subcriber有个deps属性,用来收集影响当前函数的响应式数据,例如一个watch中“使用”了三个响应式数据,那么这三个响应式数据就保存在这个deps中。
什么是link
可以看作是数据库里的中间表,用于表示dep-sub的一一对应关系。因为有了这个中间人的关系,dep和sub就不直接指向了,也就是说响应式数据的dep实际上指向的是个link,Subcriber的deps指向的也是个link,为什么这里只指向一个link而不是数组?因为link是一个链表结构的对象,而且是双链表的结构。
export class Link {
version: number
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
}
- 其中一条链表nextSub,prevSub是响应式数据的dep对象的subs属性走的,subs指向第一个link对象,然后通过链表结构遍历所有link拿到对应的sub。
- 另一条链表nextDep,prevDep是Subcriber的deps属性走的,链接着影响sub的所有响应式数据,当当前的sub的状态发生变化的时候,比如watch函数stop了,就需要遍历这个deps,把所有的link都删掉,类似数据库中间表的级联关系。
举个例子:
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
首先,count是一个由ref创建的响应式数据,此时count对象内有个dep,有个value(ref的构造函数会把入参用于初始化value),有个重写getter和setter。 其次,运行watchEffect函数,最终执行watch方法:
// packages/reactivity/src/watch.ts
export function watch() {
effect = new ReactiveEffect(getter)
...
effect.run()
}
可以看到watch方法内创建了一个ReactiveEffect,入参是getter,此时的getter就是watchEffect方法的第一个入参,也就是执行的回调函数,最后执行effect.run方法:
// packages/reactivity/src/effect.ts
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
run() {
activeSub = this
return this.fn()
}
}
ReactiveEffect实现了Subscriber,run的时候把一个全局的activeSub指向了this,然后执行this.fn,这个fn就是watchEffect送的回调函数。回调函数中打印响应式数据count,触发getter拦截:
// packages/reactivity/src/ref.ts
get value() {
this.dep.track()
return this._value
}
执行dep的track函数,用于收集watcher,返回当前的值_value。
// packages/reactivity/src/dep.ts
track() {
let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this)
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
addSub(link)
}
}
一开始,activeLink没有初始化,等于undefined,if判断link等于undefined或者activeLink的sub不等于activeSub时,activeLink指向一个新建的Link对象,构造函数传入activeSub和this,this就是当前的Dep对象,这个activeSub就是上面提到的,每个Subscriber在运行期间都会被指向。
比如一个响应式数据count在两个不同的watchEffect中被使用,在第一个watchEffect时,count的dep的activeLink已经被初始化了,在第二个watchEffect中触发count的getter拦截后,判断dep的activeLink的sub保存的是上一个watchEffect的sub,与当前的activeSub不一致,那么要新建一个Link,并把activeLink指向最后的这个Link。
从这里就能看出,每一个dep和sub都对应一个新的Link对象。
紧接着将当前响应式数据(实际上是link)收集到activeSub的deps属性里,具体的操作是:当第一个响应式数据使用时,将activeSub的deps和depsTail初始化并指向到link。在当前Subscriber执行期间使用的剩余的响应式数据,通过prevDep和nextDep进行连接。
最后addSub就是将当前activeSub(实际上是link)添加到dep的subs属性的链表中。
最后执行完所有代码后就形成了最上面图示中的数据结构。