2025.09.08
复用机制:基于链表节点的依赖去重
在前文的分析中,我们发现响应式系统存在依赖重复收集导致的指数级性能爆炸问题。问题的核心在于:每次 effect
重新执行时,它都“忘记”了自己之前收集的依赖,从而创建冗余的 Link 节点。
♻️ 链表节点复用机制:解决依赖重复收集
在前文的分析中,我们发现响应式系统存在依赖重复收集导致的指数级性能爆炸问题。问题的核心在于:每次 effect
重新执行时,它都“忘记”了自己之前收集的依赖,从而创建冗余的 Link 节点。
🎯 解决思路
最直接的解决方案是 为 effect 建立一个反向引用系统:
要实现去重和节点复用,我们必须赋予 effect “记忆” 能力——让它知道自己已经订阅了哪些数据。
核心方案:
- 建立双向链表 (
Effect<->Ref): 在原有的Ref->Link->Effect链基础上,反向建立Effect->Link->Ref链。 - 实现节点复用:
effect重新执行时,不再创建新节点,而是遍历自己的“记忆链表”(deps链),直接复用原有的Link节点。

收益: 复用机制不仅解决了重复收集问题,还为下一步的依赖精确清理(cleanup)提供了 O(1) 的基础。
🧱 数据结构升级:双重双向链表
为了实现 Effect 和 Ref 之间的双向关联,Link 节点需要存储更全面的引用信息,使它能同时存在于两个不同的链表中。
| 结构 | 核心属性 | 描述 |
|---|---|---|
ReactiveEffect (订阅者) | deps depsTail | 记录 该 effect 依赖的所有 ref (Effect-> Link )。 |
RefImpl (依赖项) | subs subsTail | 记录 所有订阅该 ref 的 effect (Ref -> Link)。 |
Link 节点 (链接桥梁) | sub dep | 指向关联的 Effect Ref )。 |
⚙️ 代码实现
- 完善
link函数: 建立双重双向引用 在收集依赖时,同步维护Ref的subs链和Effect的deps链。
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 {
// ... 恢复上下文
}
}
link函数: 实现复用逻辑 当link函数被调用时,它检查effect的deps和depsTail状态,以决定是复用还是新建。复用条件: 当 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. 否则,创建新节点并走正常的尾部追加逻辑...
}
🎉 完整执行流程
- 初次执行
effect.run()启动,deps和depsTail均被初始化为link1。flag的subs和subsTail也指向link1。
- 第一次点击
flag.value变化,触发propagate执行effect.run()。effect.run()将depsTail临时设为undefined。effect重新执行,读取flag.value触发link。link函数检测:deps存在,depsTail为undefined,且link1.dep === flag。- 复用成功:
link函数直接将depsTail重新指向link1,没有创建新节点。 - 本轮依赖收集完成后,链表结构保持不变。