2025.09.08

复用机制:基于链表节点的依赖去重

在前文的分析中,我们发现响应式系统存在依赖重复收集导致的指数级性能爆炸问题。问题的核心在于:每次 effect 重新执行时,它都“忘记”了自己之前收集的依赖,从而创建冗余的 Link 节点。

♻️ 链表节点复用机制:解决依赖重复收集

在前文的分析中,我们发现响应式系统存在依赖重复收集导致的指数级性能爆炸问题。问题的核心在于:每次 effect 重新执行时,它都“忘记”了自己之前收集的依赖,从而创建冗余的 Link 节点。

🎯 解决思路

最直接的解决方案是 effect 建立一个反向引用系统

要实现去重和节点复用,我们必须赋予 effect “记忆” 能力——让它知道自己已经订阅了哪些数据。

核心方案

  1. 建立双向链表 (Effect <-> Ref): 在原有的 Ref -> Link -> Effect 链基础上,反向建立 Effect -> Link -> Ref 链。
  2. 实现节点复用: effect 重新执行时,不再创建新节点,而是遍历自己的“记忆链表”(deps 链),直接复用原有的 Link 节点。

   收益: 复用机制不仅解决了重复收集问题,还为下一步的依赖精确清理(cleanup)提供了 O(1) 的基础。

🧱 数据结构升级:双重双向链表

为了实现 EffectRef 之间的双向关联,Link 节点需要存储更全面的引用信息,使它能同时存在于两个不同的链表中。

结构核心属性描述
ReactiveEffect (订阅者)deps depsTail记录 该 effect 依赖的所有 ref (Effect-> Link )。
RefImpl (依赖项)subs subsTail记录 所有订阅该 refeffect (Ref -> Link)。
Link 节点 (链接桥梁)sub dep指向关联的 Effect Ref )。

⚙️ 代码实现

  1. 完善 link 函数: 建立双重双向引用 在收集依赖时,同步维护 Refsubs 链和 Effectdeps 链。
export function link(dep: Dep, sub: ReactiveEffect): void {
  const newLink: Link = {
    sub,
    dep, // 🎯 关键:Link 节点记录了依赖项 (Ref)
    nextSub: undefined,
    prevSub: undefined,
    nextDep: undefined,
    prevDep: undefined // 双向链表需要 prevDep/prevSub
  }

  // 1. 维护 Ref 链 (subs 链,尾部追加)
  // ... (逻辑同前,dep.subsTail 维护)

  // 2. 维护 Effect 链 (deps 链,尾部追加)
  if (sub.depsTail) {
    sub.depsTail.nextDep = newLink
    newLink.prevDep = sub.depsTail // 建立反向链接
    sub.depsTail = newLink
  }
  else {
    sub.deps = newLink
    sub.depsTail = newLink
  }
}

🧠 节点复用机制:状态标记法

如何让 effect 知道它正在 “重新执行”,并可以开始复用旧节点?我们通过临时修改 effect 实例上的 depsTail 状态来实现标记。

// effect.ts
run() {
  const prevSub = activeSub

  activeSub = this
  this.depsTail = undefined // 💥 关键标记:清除尾指针,表示进入复用模式

  try {
    return this.fn()
  } finally {
    // ... 恢复上下文
  }
}
  1. link 函数: 实现复用逻辑 当 link 函数被调用时,它检查 effectdepsdepsTail 状态,以决定是复用还是新建。
      复用条件: 当 effect 存在 deps (头节点) 但 depsTail 为 undefined 时,意味着它正在重新收集,且之前有依赖需要复用。
    
// system.ts -> link 函数片段
export function link(dep, sub) {
  const nextDep = sub.depsTail === undefined ? sub.deps : undefined // 当前链表的下一个待复用节点

  // 1. 检查是否可复用
  if (nextDep && nextDep.dep === dep) {
    // 2. 🎯 复用成功:确认当前节点就是上次收集的 ref
    sub.depsTail = nextDep // 将尾指针重新指向该节点
  }

  // 3. 否则,创建新节点并走正常的尾部追加逻辑...
}

🎉 完整执行流程

  1. 初次执行
    • effect.run() 启动,depsdepsTail 均被初始化为 link1
    • flagsubssubsTail 也指向 link1
  2. 第一次点击
    • flag.value 变化,触发 propagate 执行 effect.run()
    • effect.run()depsTail 临时设为 undefined
    • effect 重新执行,读取 flag.value 触发 link
    • link 函数检测:deps 存在,depsTailundefined,且 link1.dep === flag
    • 复用成功: link 函数直接将 depsTail 重新指向 link1,没有创建新节点。
    • 本轮依赖收集完成后,链表结构保持不变。