2025.09.12

优雅清理:过期依赖的防残留解决方案

🎯 问题回顾

effect 的执行路径是动态变化的。当条件分支或逻辑发生切换时,effect 可能会遗弃部分旧的响应式依赖(ref)。

如果这些“过期依赖”未被及时清理,将导致双重危害:

  1. 内存泄漏: 废弃的双向链表节点持续占用内存空间。
  2. 无效的更新: effect 实际上已不再访问某个 ref,但该 ref 的变化仍会触发 effect 重新执行,造成性能浪费和逻辑错误。

核心解决思路: 在 effect 每次执行周期结束时,通过检查依赖链表的尾部状态,一次性识别并移除所有本次执行中未被访问到的旧依赖。

💡 清理策略

通过追踪 effect 运行时访问到的最后一个依赖节点 (depsTail),来确定哪些节点是“过期”的。

  1. 场景一: 条件型依赖切换 在 effect 运行时,新的依赖(如 link3)会被追加到链表末尾,并更新 depsTail

判断依据: 如果 depsTail 存在,且其 nextDep 引用不为空 (depsTail.nextDep 存在),则表示从 depsTail.nextDep 开始直到链表末尾的所有节点,都是本次执行中未被访问到的旧依赖,必须进行清理。

  1. 场景二: effect 提前 return 如果 effect 在访问任何响应式数据之前就因提前 return 或其他逻辑而中止,将导致:
    1. depsTail 保持其初始状态:undefined
    2. 旧的依赖链表头节点 (sub.deps) 依然存在。
      判断依据: 如果 depsTailundefined,但 sub.deps 头节点仍然存在,则表示本次执行“颗粒无收”但残留着旧依赖,必须清空全部依赖。

🛠️ 代码实现

引入 startTrackendTrack 这对“门卫”函数,将依赖清理逻辑封装到 effect 的执行周期内。

// effect.ts 核心结构
export class ReactiveEffect {
  run() {
    // ... 准备工作 ...
    startTrack(this) // ❶ 清理准备
    try {
      return this.fn() // ❷ 核心执行(重新收集依赖)
    }
    finally {
      endTrack(this) // ❸ 依赖清理
      // ... 恢复工作 ...
    }
  }
}

export function startTrack(sub: ReactiveEffect) {
  // 每次运行前,重置 depsTail,准备追踪本次运行的最后一个依赖
  sub.depsTail = undefined
}

export function endTrack(sub: ReactiveEffect) {
  const depsTail = sub.depsTail

  if (depsTail) {
    // 场景 1: depsTail 存在 (有收集到依赖)
    if (depsTail.nextDep) {
      // 存在 nextDep,说明从它开始是旧依赖
      clearTracking(depsTail.nextDep) // 清理过期依赖
      depsTail.nextDep = undefined
    }
  }
  else if (sub.deps) {
    // 场景 2: depsTail 不存在,但 deps 链表头存在 (提前返回/零收集)
    clearTracking(sub.deps) // 清空所有旧依赖
    sub.deps = undefined
  }
}

🔗 clearTracking:双向链表移除

clearTracking 的核心任务是安全地解除双向引用:effect.deps(依赖链表)和 dep.subs(订阅链表)上的引用关系。

关键步骤

  1. 遍历: 循环处理待清理的每一个 link 节点
  2. 移除定的关系(dep.subs): 基于 prevSubnextSub 指针, 将当前 link节点从 ref 的订阅者链表中删除
    • 如果是头节点, 更新 deps.subs 指针
    • 如果是中间/尾 节点, 更新相邻节点的 nextSubprevSub 指针
  3. 解除引用: 清空 link 节点上执行 depsub 和相邻节点的引用
  4. 移动: 通过 nextDep 指针移动到下一个待清理的节点
function clearTracking(link: Link) {
  while (link) {
    const { prevSub, nextSub, nextDep, dep } = link

    // 1. 从 dep.subs (订阅链表) 中移除当前 link 节点
    if (prevSub) {
      prevSub.nextSub = nextSub // 前一个节点的 nextSub 指向后一个节点
    }
    else {
      dep.subs = nextSub // 当前是头节点,更新 dep.subs
    }

    if (nextSub) {
      nextSub.prevSub = prevSub // 后一个节点的 prevSub 指向前一个节点
    }
    else {
      dep.subsTail = prevSub // 当前是尾节点,更新 dep.subsTail
    }

    // 2. 解除 link 节点上的引用
    link.dep = link.sub = undefined
    link.nextSub = link.prevSub = undefined

    // 3. 移动到 effect.deps (依赖链表) 中的下一个待清理节点
    link = nextDep
  }
}