Appearance
dep和sub双向连接
看一个示例:
ts
const flag = ref(false)
effect(() => {
console.count(flag.value)
})
btn.onClick(() => {
flag.value = !flag.value
})上述代码执行完成后flag的依赖链表节点如下:
当点击按钮,会触发effect重新执行,这次执行,必然会触发flag的get,从而在次收集依赖,链表的结构就变成了这样:
此时flag的订阅者链表中已经有了两个节点,并且他们同时指向一个effect,这必然会导致一次更行触发多次effect。
既然dep可以通过链表的节点找到sub,那么也可以让sub通过链表的节点找到dep,这样每次重新执行的时候,看一下sub和dep之间,有没有关联,如果有,就不重新创建了。于是给链表设计成了这种结构:
ts
/**
* 依赖项
*/
interface Dep {
// 订阅者链表头节点
subs: Link | undefined
// 订阅者链表尾结点
subsTail: Link | undefined
}
// 订阅者
interface Sub {
// 依赖项链表的头节点
deps: Link | undefined
// 依赖项链表尾结点
depsTail: Link | undefined
}
// 链表节点
export interface Link {
// 订阅者
sub: Sub
// 下一个订阅者节点
nextSub: Link | undefined
// 上一个订阅者节点
pervSub: Link | undefined
// 依赖项
dep: Dep
// 下一个依赖项节点
nextDep: Link | undefined
}这样链表里面就同时保存了dep和sub,并且给ReactiveEffect加一个单向链表,让他在重新执行时,可以找到自己之前收集到的依赖,尝试复用:
ts
class ReactiveEffect {
// 依赖项链表头节点
deps: Link | undefined
// 依赖项链表尾结点
depsTail: Link | undefined
// 省略后续代码...
}然后在effect执行的时候,要创建sub和link之间的关联关系。怎么创建?和dep一样,在link中创建:
ts
// 链表连接关系
export function link(dep, sub) {
const newLink = {
sub,
dep,
nextDep: undefined,
nextSub: undefined,
pervSub: undefined,
}
if (dep.subsTail) {
// 如果尾结点存在,证明不是第一次触发,那么就把新节点加到尾结点后面
dep.subsTail.nextSub = newLink
// 把新节点的prevSub指向当前的尾结点
newLink.prevSub = dep.subsTail
// 更新当前的尾结点为最新的link
dep.subsTail = newLink
} else {
// 如果尾结点不存在,证明是第一次触发,那么就把新节点作为头结点和尾结点
dep.subs = newLink
dep.subsTail = newLink
}
if(sub.depsTail) {
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
} else {
sub.deps = newLink
sub.depsTail = newLink
}
}这样首次运行完之后,结构就变成了这样:
那么当effect执行完毕后,会收集依赖。当effect被通知更新的时候,把depsTail设置成undefined
那么现在,depsTail指向undefined,deps指向link1。这种情况下,可以视为它之间收集过依赖,在重新执行的时候,需要尝试复用。尝试先复用第一个,然后一次往后。
ts
class ReactiveEffect {
run() {
// 先将当前的 effect 保存起来,用来处理嵌套的逻辑
const prevSub = activeSub
// 每次执行fn之前,把this放到activeSub上
activeSub = this
// 开始执行前,将depsTail变成undefined
this.depsTail = undefined
try {
return this.fn()
} finally {
// 执行完成后,恢复之前的 effect activeSub = prevSub
}
}
}那么这个时候,就是第一种情况,头节点有,尾结点没有,进行尝试复用依赖项:
ts
export function link(dep, sub) {
const currentDep = sub.depsTail
// 如果尾节点没有,头节点有,那我们拿到头节点
const nextDep = currentDep === undefined ? sub.deps : undefined
// 看一下头节点有没有,如果头节点也有,那么我们看一下头节点的 dep 是不是等于当前我们要收集的 dep
if (nextDep && nextDep.dep === dep) {
// 相同,将尾节点指向头节点
sub.depsTail = nextDep
return
}
}还有另一种情况:就是多个依赖,此时肯定尾结点还是有nextDep的,于是需要进一步复用nextDep:
ts
export function link(dep, sub) {
//region 尝试复用链表节点
const currentDep = sub.depsTail
/**
* 分两种情况:
* 1. 如果头节点有,尾节点没有,那么尝试着复用头节点
* 2. 如果尾节点还有 nextDep,尝试复用尾节点的 nextDep
*/
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep
return
}
//endregion
}总结需要复用 link 节点的两种情况:
- 尾结点没有,头节点有,此时需要复用头节点
- 尾结点有
nextDep需要进一步复用nextDep
节点复用
ts
/**
* 依赖项
*/
interface Dep {
// 订阅者链表的头节点
subs: Link | undefined
// 订阅者链表的尾节点
subsTail: Link | undefined
}
/**
* 订阅者
*/
interface Sub {
// 依赖项链表的头节点
deps: Link | undefined
// 依赖项链表的尾节点
depsTail: Link | undefined
}
/**
* 链表节点
*/
export interface Link {
// 订阅者
sub: Sub
// 下一个订阅者节点
nextSub: Link | undefined
// 上一个订阅者节点
prevSub: Link | undefined
// 依赖项
dep: Dep
// 下一个依赖项节点
nextDep: Link | undefined
}每次触发effect,都需要重新收集依赖,但是针对那些已经收集过的依赖,就不需要重新收集了。所以需要尝试复用收集过的依赖。但是需要知道,当前次执行需要复用的依赖项,所以,每次开始执行fn的时候,先把depsTail置空
ts
class ReactiveEffect {
run() {
// 先将当前的 effect 保存起来,用来处理嵌套的逻辑
const prevSub = activeSub
// 每次执行 fn 之前,把 this 放到 activeSub 上面
activeSub = this
// ⭐️ 每次执行 fn 之前,先把 depsTail 置空
this.depsTail = undefined
try {
return this.fn()
} finally {
// 执行完成后,恢复之前的 effect
activeSub = prevSub
}
}
}这样就可以明确知道,一下两种情况是需要复用节点的: 
复用逻辑
ts
// 先拿到尾节点
const currentDep = sub.depsTail
/**
* 分两种情况:
* 1. 如果头节点有,尾节点没有,那么尝试着复用头节点
* 2. 如果尾节点还有 nextDep,尝试复用尾节点的 nextDep
*/ const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep
return
}头节点复用
当depsTail为undefined时,会尝试复用deps(头结点)。
nextDep复用
当depsTail存在时,会尝试复用depsTail.nextDep,就是nextDep2
复用的核心条件是:
ts
if(nextDep && nextDep.dep === dep)这个条件确保:
- 待复用的节点存在
- 待复用节点的依赖项与当前依赖项相同
补充:
deps有,depsTail没有,表示收集的第一个依赖,因为在执行effect.fn之前,已经把depsTail设置成undefined了,所以这个时候尝试复用链表的头节点,链表头结点复用完成后,需要把depsTail指向当前已经复用成功的节点(就是deps),此时头尾相同,注意:此时有可能depsTail还有nextDep,因为头节点可能有下一个节点- 如果
depsTail.nextDep有,表示本次执行是重新执行,并且头节点已经复用完毕,此时需要尝试复用的节点就是depsTail.nextDep
复用失败后的处理
如果无法复用节点,系统会创建新的链表节点并建立双向连接:
ts
const newLink = {
sub,
dep,
nextDep,
nextSub: undefined,
prevSub: undefined
}然后分别建立与dep和sub的关联关系:
与dep建立关联

与sub建立关联
