爱听播放器软件
40.14MB · 2025-09-30
昨天我们了解到,当 effect
函数依赖多个响应式变量时,会再次触发指数级更新。
我们来回顾一下之前的做法:
run()
函数首先会将 depsTail
设为 undefined
。
// effect.ts
run(){
const prevSub = activeSub
activeSub = this
// 开始执行,将尾节点设为 undefined
this.depsTail = undefined
// ...
}
后续的依赖收集中,depsTail
会被赋值并指向已复用的节点。
export function link(dep, sub){
if(sub.depsTail === undefined && sub.deps){
if(sub.deps.dep === dep){
sub.depsTail = sub.deps // 移动尾节点指针,指向刚刚复用的节点
return
}
}
// ...
}
完整的判断逻辑:
export function link(dep, sub){
const currentDep = sub.depsTail;
// 仅在 depsTail 为 undefined 时,才尝试从头节点 sub.deps 开始复用
if (currentDep === undefined && sub.deps) {
// 只会检查依赖链表的第一个节点
if (sub.deps.dep === dep) {
sub.depsTail = sub.deps; // 成功后移动指针
return;
}
}
// 若不符合上述条件,则直接创建新节点...
}
深入分析后,我们了解到问题出在依赖节点的复用逻辑上:
sub.deps
)。flag
)复用成功,depsTail
就被赋值。这导致后续依赖(例如 count
)在检查时,因为 currentDep === undefined
条件不成立,直接跳过了复用检查,从而盲目地创建了新的 Link
节点。在旧的逻辑中,depsTail
只是一个标记,用于判断是否是“第一次执行复用”。现在我们要把它升级为一个“进度指针”。它的作用是标记当前复用检查进行到了链表的哪个位置。
这个思路提供了一个关键点:“当 depsTail
存在时,代表依赖链表的遍历与复用正在进行中。 ”
因此,我们就可以在原有的 link
函数中,增加一个额外的检查逻辑。
当第一个 if
条件不成立,但 depsTail
(currentDep
) 确实存在时,就意味着我们不应该从头节点开始检查,而应该从当前 depsTail
所在节点的下一个节点 (currentDep.nextDep
) 继续检查。
依照上面的执行逻辑,flag
复用成功后,depsTail
指向 Link1
。我们需要新增的逻辑,就是要从 Link1
的 nextDep
,也就是 Link2
,继续进行检查。
检查逻辑的核心:如果尾节点 (depsTail
) 存在,并且这个尾节点还有下一个节点 (nextDep
),我们就应该检查这个 nextDep
是否是我们要找的目标,如果是,就直接复用它。
depsTail
是否有值。如果有,代表复用已经开始,并且当前进度停在链表的某个节点上(像是 Link1
)。depsTail
所指向节点的“下一个节点”,也就是 currentDep.nextDep
(对应到我们的例子,就是 Link2
)。currentDep.nextDep.dep
),是否就是我们当前正要处理的依赖 (count
)。depsTail
这个“进度指针”向前移动到这个 nextDep
节点上。const currentDep = sub.depsTail
if (currentDep === undefined && sub.deps) {
// 依赖链表头节点的 ref 与当前要连接的 ref 相等的话,表示之前收集过依赖
if (sub.deps.dep === dep) {
sub.depsTail = sub.deps // 移动尾节点指针,指向刚刚复用的节点
return // 直接返回,不新增节点
}
} else if (currentDep) { // 尾节点存在
// 尾节点的 nextDep 所连接的 ref,等于当前要连接的 ref
if (currentDep.nextDep?.dep === dep) {
sub.depsTail = currentDep.nextDep
// 移动尾节点指针,复用尾节点的 nextDep
return
}
}
目前这个结构虽然已经能正确运行,但我们可以将它重构得更简洁。我们发现无论是从头开始还是从中途继续,我们的目标都是找到“下一个待检查节点” 。
const currentDep = sub.depsTail
// 核心逻辑:根据 currentDep 是否存在,来决定下一个要检查的节点
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// 如果 nextDep 存在,且 nextDep.dep 等于我当前要收集的 dep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep // 移动指针
return
}
获取 depsTail
当前值 (currentDep
= sub.depsTail
)。
根据 depsTail
决定要检查哪个节点:
depsTail
为 undefined
→ 从头节点开始 (nextDep
= sub.deps
)。depsTail
有值 → 检查下一个 (nextDep
= currentDep.nextDep
)。检查是否可以复用 (nextDep
必须存在且其 .dep
属性与当前依赖匹配)。
如果可以复用:
depsTail
到 nextDep
(记录遍历进度)。Link
,提前返回。通过将 depsTail
指针从一个单纯的“尾部标记”升级为“遍历进度指针”,我们解决了多变量依赖下的节点复用问题。
重构后的代码不仅修复了指数级更新的 Bug,更用统一的逻辑处理了不同情况下的节点检查。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。