当响应式数据变化时,Vue 需要精准通知到依赖它的副作用函数(effect)。但如果一个 effect 中依赖的数据发生变化,会导致 effect 重新执行,而新的执行可能产生不同的依赖集合。此时旧的依赖如果继续保留,就会造成无效更新甚至内存泄漏。为了解决这个问题,Vue 实现了依赖清理和避免重复收集两套机制。
为什么需要清理依赖
考虑以下组件逻辑:
const useToggle = () => {
const state = reactive({ flag: true, a: 'hello', b: 'world' })
effect(() => {
if (state.flag) {
console.log(state.a)
} else {
console.log(state.b)
}
})
}
首次执行 effect 时,state.flag 为 true,读取了 state.a,因此该 effect 被收集到 state.a 的依赖集合中。随后如果 state.flag 变为 false,effect 重新执行,这次读取的是 state.b,不再读取 state.a。如果不做清理,那么后续修改 state.a 仍会触发 effect 执行,但此时 effect 的逻辑根本不关心 state.a——这既是浪费,也可能导致预料之外的副作用。
清理机制:在每次执行 effect 前清除旧依赖
Vue 3 的 effect 函数在每次执行前,都会调用一个 cleanup 步骤,将当前 effect 从它之前订阅的所有依赖集合中移除。具体实现方式是:每个 effect 维护一个 deps 数组,记录它当前订阅的所有 Dep(即响应式属性的依赖集合)。当重新执行时,遍历 deps,把该 effect 从每一个 Dep 中删除,然后重新执行,在执行过程中通过 track 重新收集新的依赖。这样旧依赖被清空,新依赖被建立,保证 effect 只在真正需要时被触发。
伪代码表示如下:
function effect(fn) {
const reactiveEffect = () => {
cleanup(reactiveEffect) // 清除旧依赖
activeEffect = reactiveEffect
fn() // 重新执行,触发 track 重新收集
}
reactiveEffect.deps = [] // 存储该 effect 订阅的所有 Dep
reactiveEffect()
}
function cleanup(effect) {
effect.deps.forEach(dep => dep.delete(effect))
effect.deps.length = 0
}
track 函数负责在读取响应式数据时,将当前 activeEffect 添加到该数据对应的 Dep 中,同时将该 Dep 存入 effect 的 deps 数组,形成双向记录。这样 cleanup 时就能精确定位到需要删除的位置。
避免重复收集
同一个 effect 可能多次访问同一个响应式属性,例如:
effect(() => {
console.log(state.a + state.a)
})
如果不做处理,state.a 的依赖集合中,同一个 effect 会被重复添加两次。一旦 state.a 变化,effect 会被重复调用两次,导致不必要的开销。
Vue 通过以下两种方式防止重复收集:
- Set 管理依赖
每个响应式属性维护一个 Dep 实例,它的内部使用 Set 存储 effect。Set 本身就保证了元素不重复。即使多次调用 dep.add(effect),集合中也始终只有一个引用。
- track 时的双向检查
在执行 track 时,除了将 effect 加入 Dep,还会检查该 effect 的 deps 数组中是否已经存在相同的 Dep 对象。如果存在,则跳过后续操作,避免重复建立关联。
结合清理机制,每次 effect 执行前会清理所有旧的 Dep 关联,并在新的执行中重建关联。重建过程中 Set 保证了重复访问不会导致重复收集,从而让整个依赖系统始终精确、最小化。
实用价值
这套机制对开发者是透明的,但理解它能帮助避免一些不必要的困扰:
- 条件分支中的依赖变化会自动调整:我们不需要手动管理依赖,Vue 自动保证 effect 始终只订阅最后一次执行所访问的数据。
- 不会造成内存泄漏:如果组件销毁时 effect 被停止(通过
effectScope或组件卸载时的清理),其关联的所有 Dep 都会被移除,响应式数据不再持有该 effect 的引用,GC 可以正常回收。 - 性能保证:重复收集的防止保证了更新派发时的遍历次数最优,不会出现一个 effect 被多次调用的情况。
简单说,依赖清理让响应式系统拥有“自净”能力,避免陈旧依赖造成无效更新;而重复收集预防则确保每个依赖关系都是干净、单一的。两者配合,让 Vue 3 的响应式既灵活又健壮。