浅谈Vue3的dep,sub,link之间的关系

0 阅读4分钟

话不多说,先上图

Vue3 reactive.jpg

首先,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属性的链表中。

最后执行完所有代码后就形成了最上面图示中的数据结构。