昨天我们了解到,当 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」,一起跟日安当同学。