ZuB1M1H.png

昨天我们了解到,当 effect 函数依赖多个响应式变量时,会再次触发指数级更新。

我们来回顾一下之前的做法:

  1. run() 函数首先会将 depsTail 设为 undefined

    // effect.ts
    run(){
        const prevSub = activeSub
        activeSub = this
    
        // 开始执行,将尾节点设为 undefined
        this.depsTail = undefined
    
        // ...
      }
    
  2. 后续的依赖收集中,depsTail 会被赋值并指向已复用的节点。

    export function link(dep, sub){
      if(sub.depsTail === undefined && sub.deps){
        if(sub.deps.dep === dep){
          sub.depsTail = sub.deps // 移动尾节点指针,指向刚刚复用的节点
          return
        }
      }
      // ...
    }
    
  3. 完整的判断逻辑:

    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) 继续检查。

day13-01.png

依照上面的执行逻辑,flag 复用成功后,depsTail 指向 Link1。我们需要新增的逻辑,就是要从 Link1nextDep,也就是 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
}

完整执行流程

  1. 获取 depsTail 当前值 (currentDep = sub.depsTail)。

  2. 根据 depsTail 决定要检查哪个节点:

    • depsTailundefined → 从头节点开始 (nextDep = sub.deps)。
    • depsTail 有值 → 检查下一个 (nextDep = currentDep.nextDep)。
  3. 检查是否可以复用 (nextDep 必须存在且其 .dep 属性与当前依赖匹配)。

  4. 如果可以复用:

    • 移动 depsTailnextDep (记录遍历进度)。
    • 不再创建新的 Link,提前返回。

通过将 depsTail 指针从一个单纯的“尾部标记”升级为“遍历进度指针”,我们解决了多变量依赖下的节点复用问题。

重构后的代码不仅修复了指数级更新的 Bug,更用统一的逻辑处理了不同情况下的节点检查。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]