Vue3 的响应式系统源码级副作用清理(cleanup)与嵌套 effect 处理原理
字数 991 2025-11-16 03:15:12
Vue3 的响应式系统源码级副作用清理(cleanup)与嵌套 effect 处理原理
1. 问题描述
在 Vue3 的响应式系统中,副作用函数(effect)可能会依赖多个响应式数据,当依赖变化时需重新执行 effect。但若依赖关系动态变化(如条件分支中依赖不同数据),需清理无效依赖以避免冗余更新。同时,effect 可能嵌套(如组件渲染嵌套),需正确建立依赖关系与清理机制。
2. 核心概念:副作用函数与依赖收集
- 副作用函数(effect):封装响应式数据变化的执行逻辑(如组件渲染、计算属性)。
- 依赖收集:通过 Proxy 的 getter 拦截,将当前执行的 effect 存入响应式数据的依赖集合(Set)中。
- 触发更新:数据修改时,从依赖集合中取出所有 effect 重新执行。
3. 为什么需要清理依赖?
场景示例
const state = reactive({ flag: true, a: 1, b: 2 })
effect(() => {
if (state.flag) {
console.log(state.a) // 依赖 a
} else {
console.log(state.b) // 依赖 b
}
})
初始时 flag=true,effect 依赖 flag 和 a。若将 flag 改为 false,effect 应依赖 flag 和 b,但旧依赖 a 仍保留。若修改 a,会错误触发 effect 重新执行。
4. 源码级清理机制实现
(1)effect 的结构设计
let activeEffect // 当前正在执行的 effect
class ReactiveEffect {
constructor(fn) {
this.fn = fn
this.deps = [] // 存储所有包含本 effect 的依赖集合(Set)
}
run() {
activeEffect = this
cleanupEffect(this) // 执行前清理旧依赖
return this.fn()
}
}
(2)清理函数 cleanupEffect
function cleanupEffect(effect) {
const { deps } = effect
for (const dep of deps) {
dep.delete(effect) // 从所有依赖集合中移除本 effect
}
effect.deps.length = 0 // 清空 deps 数组
}
(3)依赖收集的双向关联
在 Proxy getter 中调用 track 函数:
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
dep.add(activeEffect)
activeEffect.deps.push(dep) // 将依赖集合记录到 effect.deps
}
关键:每个 effect 通过 deps 记录自己属于哪些依赖集合(Set),方便后续清理。
5. 嵌套 effect 的处理原理
场景示例(组件嵌套)
effect(() => {
console.log('父效应')
effect(() => {
console.log('子效应')
})
})
解决方案:effectStack
let effectStack = [] // 栈结构管理嵌套 effect
class ReactiveEffect {
run() {
if (effectStack.includes(this)) return // 避免无限递归
try {
effectStack.push(this)
activeEffect = this
cleanupEffect(this)
return this.fn()
} finally {
effectStack.pop() // 执行完成后出栈
activeEffect = effectStack[effectStack.length - 1] // 恢复父 effect
}
}
}
流程:
- 执行父 effect 前,将其入栈并设为
activeEffect。 - 子 effect 执行时,父 effect 暂存于栈中,子 effect 成为新的
activeEffect。 - 子 effect 执行完毕出栈,恢复父 effect 为
activeEffect。
6. 综合流程示例
const state = reactive({ flag: true, a: 1, b: 2 })
effect(() => {
console.log('effect运行')
if (state.flag) {
console.log('依赖a:', state.a)
} else {
console.log('依赖b:', state.b)
}
})
// 初始:effect 依赖 flag 和 a
state.flag = false // 触发 effect 重新运行
// 重新收集依赖:清理旧依赖(a)后,新依赖为 flag 和 b
state.a = 100 // 不会触发 effect(因已清理 a 的依赖)
7. 总结
- 依赖清理:通过
effect.deps记录所有依赖集合,每次执行 effect 前清理旧依赖,避免无效更新。 - 嵌套处理:利用栈结构管理嵌套 effect,确保依赖收集指向正确的
activeEffect。 - 性能优化:避免冗余依赖导致不必要的重渲染,提升响应式系统精确性。