2025.09.07
重复收集陷阱:Effect 触发的性能危机
💥 问题重现:失控的 effect
经过模块化重构的响应式系统,在与 DOM 交互结合时,暴露了一个严重的性能问题:每次触发更新,effect 的执行次数都呈指数级增长。
示例代码与现象:
<button id="btn">切换状态</button>
<script type="module">
import {ref, effect} from '../dist/reactivity.esm.js'
const flag = ref(true)
effect(() => {
flag.value // 依赖收集点
console.count('effect 触发次数') // 计数器
})
btn.onclick = () => {
flag.value = !flag.value // 触发更新
}
</script>
运行结果显示:第一次点击执行 1 次,第二次点击执行 2 次,第三次 4 次,第四次 8 次... 触发次数呈 $2^n$ 增长。

🔬 深入分析:重复收集的恶性循环
这个问题的根源在于 每次 effect 重新执行时,它都会进行重复的依赖收集。
- 初始化与首次收集
- 操作: 页面首次加载,
effect.run()执行 - 结果: 读取
count.value触发getter - 依赖链: 创建
link1,flag成功依赖effect - 链表长度: 1

- 操作: 页面首次加载,
- 首次点击: 依赖翻倍
- 操作:
flag.value变化,触发setter,propagate执行。 - 触发: 找到
link1,执行effect.run()(1 次)。 - 重新收集:
effect再次运行,再次读取flag.value,又触发getter。 - 结果: 重复收集,创建
link2。 - 链表长度: 2(包含两个指向同一个
effect的链接节点)。
- 操作:
- 第二次点击: 指数级翻倍
- 操作: 再次点击按钮,触发
setter。 - 触发:
propagate发现链表长度为 2,因此effect.run()被执行 2 次。 - 重新收集: 两次执行,导致创建 2 个 新链接节点。
- 链表长度: 4(每次点击都使链表长度翻倍,下次将执行 4 次)。

- 操作: 再次点击按钮,触发
💡 问题本质与解决方案
核心问题: 重复收集与盲目执行
- 盲目添加: 每次
getter触发依赖收集时,都 没有检查 当前effect是否已存在。 - 恶性循环: 链表长度 L -> 触发次数 L -> 新增节点数 L -> 下次链表长度 L + L。
- 本质: 我们的双向链表没有实现
Set(集合) 的去重特性。
🛡️ 解决方案
要解决这个问题,我们需要在依赖收集过程中实现去重机制:
思路: 在收集依赖之前,必须检查当前 effect 是否已经存在于该响应式数据的依赖链中。
通过实现去重机制,我们能确保:一个响应式数据,对同一个 effect 只收集一次依赖。