2025.09.12
优雅清理:过期依赖的防残留解决方案
🎯 问题回顾
effect 的执行路径是动态变化的。当条件分支或逻辑发生切换时,effect 可能会遗弃部分旧的响应式依赖(ref)。
如果这些“过期依赖”未被及时清理,将导致双重危害:
- 内存泄漏: 废弃的双向链表节点持续占用内存空间。
- 无效的更新:
effect实际上已不再访问某个ref,但该ref的变化仍会触发effect重新执行,造成性能浪费和逻辑错误。
核心解决思路: 在 effect 每次执行周期结束时,通过检查依赖链表的尾部状态,一次性识别并移除所有本次执行中未被访问到的旧依赖。
💡 清理策略
通过追踪 effect 运行时访问到的最后一个依赖节点 (depsTail),来确定哪些节点是“过期”的。
- 场景一: 条件型依赖切换
在
effect运行时,新的依赖(如link3)会被追加到链表末尾,并更新depsTail。
判断依据: 如果 depsTail 存在,且其 nextDep 引用不为空 (depsTail.nextDep 存在),则表示从 depsTail.nextDep
开始直到链表末尾的所有节点,都是本次执行中未被访问到的旧依赖,必须进行清理。

- 场景二:
effect提前return如果effect在访问任何响应式数据之前就因提前return或其他逻辑而中止,将导致:depsTail保持其初始状态:undefined。- 旧的依赖链表头节点 (
sub.deps) 依然存在。
判断依据: 如果depsTail为undefined,但sub.deps头节点仍然存在,则表示本次执行“颗粒无收”但残留着旧依赖,必须清空全部依赖。
🛠️ 代码实现
引入 startTrack 和 endTrack 这对“门卫”函数,将依赖清理逻辑封装到 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(订阅链表)上的引用关系。
关键步骤
- 遍历: 循环处理待清理的每一个
link节点 - 移除定的关系(
dep.subs): 基于prevSub和nextSub指针, 将当前link节点从ref的订阅者链表中删除- 如果是头节点, 更新
deps.subs指针 - 如果是中间/尾 节点, 更新相邻节点的
nextSub和prevSub指针
- 如果是头节点, 更新
- 解除引用: 清空
link节点上执行dep、sub和相邻节点的引用 - 移动: 通过
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
}
}