2025.09.11
依赖有效性:响应式系统的“依赖清理”挑战
🧐 什么是依赖有效性?
在 Vue 响应式系统中,一个 effect(例如 computed 或组件渲染函数)的执行路径是动态的。它依赖的响应式数据(ref 或
reactive 对象)集合可能会随着执行时的条件判断或逻辑分支而变化。
依赖有效性关注的核心问题是:如何确保 effect 只保留当前执行路径中真正需要的依赖,并及时清除不再需要的“过期依赖”?
如果“过期依赖”没有被正确清理,将导致以下两个主要问题:
- 🗑️ 内存泄漏 (
Memory Leak): 不再需要的依赖关系(如链表节点)持续占用内存,无法被垃圾回收。 - 🔄 不必要的更新 (
Unnecessary Update):effect实际上已不再访问某个ref,但该ref的变化仍会触发effect重新执行,造成性能浪费和逻辑错误。
💡 异常情况 1: 条件型依赖的切换
以下代码展示了基于 flag 的条件渲染,使得 name 和 age 成为互斥的条件依赖:
<div id="app"></div>
<button id="flagBtn">update flag</button>
<script type="module">
const flag = ref(true)
const name = ref('zhangsan')
const age = ref(12)
effect(() => {
console.count('effect')
if (flag.value) {
app.innerHTML = `姓名: ${name.value}!` // 依赖 name
} else {
app.innerHTML = `年龄: ${age.value}!` // 依赖 age
}
})
// ... 按钮点击事件 ...
</script>

🔍 异常现象分析
- 首次执行(
flag = true):effect收集到依赖:flagname - 点击
flagBtn(flag = false):effect重新运行, 进else, 收集到新依赖flagage
- 异常结果: 此时
effect运行路径已不访问name, 但在name的订阅集合(Subs)中, 仍然保留着执行该effect的连接
| 步骤 | 示意图 | 说明 |
|---|---|---|
| ① 首次执行 | ![]() | 收集 flag 依赖,建立 link1 |
② 进入 if 分支 | ![]() | flag 为 true,继续收集 name 依赖,建立 link2 |
③ 点击按钮更新 flag | ![]() | effect 重新运行,depsTail 被重置为 undefined,准备重新收集依赖 |
④ 复用 flag 节点 | ![]() | 检测到 link1 可复用,depsTail 指向 link1,此时 flag 值变为 false |
⑤ 收集新依赖 age | ![]() | 进入 else 分支,新建 link3 并追加到 deps 链表尾部 |
❗ 根本原因: 旧的依赖(name)在 effect 自身的依赖链表 (deps) 中被移除,但在 name 的订阅者链表 (subs) 中未被解除引用,导致
name 变化时仍会错误地通知 effect。
💡 异常情况 2: effect 提前返回/中止
以下代码在 effect 首次运行后,通过 count > 0 的条件实现提前返回,理论上阻止了后续的依赖收集:
<script type="module">
// ... ref 定义 ...
let count = 0
effect(() => {
console.count('effect')
if (count > 0) return // ❗ 提前返回
count++
// ... 首次执行时会访问 flag 和 name ...
})
// ... 按钮点击事件 ...
</script>
期望行为: effect 运行一次(或两次,取决于何时进行依赖清理),之后任何依赖的变更都不应触发它。 实际现象: 持续点击 nameBtn,
console.count('effect') 计数不断增加。
🔍 异常现象分析
- 首次执行(
count = 0)count++变为 1- 执行后续逻辑, 收集到
flag和name依赖 effect.deps链表结构:flag -> name
- 点击
nameBtn触发更新:effect重新执行run(), 但在函数体执行前, 会重置依赖收集器(depsTail) 设为undefined- 函数体执行时遇到
if(count > 0) return立即返回 - 本次执行没收集到任何依赖
- 异常结果:
- 虽然本次执行没有收集依赖, 但旧的依赖(
flag和name)并没被清理 - 在
name的订阅集合中, 该effect依然存在 name变化通知effect,effect重新执行, 进入提前返回, 形成不必要的循环
- 虽然本次执行没有收集依赖, 但旧的依赖(
❗ 根本原因: effect 在每次重新执行时,必须先完整地清理所有旧的依赖,然后再根据当前执行路径重新收集。如果执行提前返回,导致新的依赖收集过程被中止,而旧依赖又没有被清理干净,就会残留“过时”的订阅关系。
⚙️ 解决方案
在 effect 重新执行之前, 需要:
- 遍历该
effect之前收集到的所有依赖 (通过effect.deps链表) - 解除
effect与这些依赖(如name或age的subs列表之间的双向引用关系) - 重置
effect的依赖链表头和尾指针, 回到 "待收集" 状态
这样,即使 effect 提前返回或进入新的条件分支,残留的旧依赖也会在执行前被完全移除,从而避免内存泄漏和不必要的更新。




