2025.09.05
执行边界:Effect 上下文管理与边界
🤯 问题场景:嵌套的副作用函数
在响应式系统中,一个 effect 函数内部调用另一个 effect 是一个常见且复杂的边界情况。这不仅发生在手动嵌套时,也是
computed 属性 等高级功能内部的工作方式。
让我们通过一个示例来重现这个问题:
const count = ref(0)
effect(() => {
// 🔴 外层 effect (fnA)
effect(() => {
// 🔵 内层 effect (fnB)
console.log('count.value1 (fnB) ==> ', count.value)
})
console.log('count.value2 (fnA) ==> ', count.value)
})
setTimeout(() => {
count.value = 1 // 触发更新
}, 1000)
实际输出与问题诊断
在未优化的基础实现中,实际输出结果揭示了一个严重的依赖丢失问题:
// 初始执行
count.value1 (fnB) ==> 0
count.value2 (fnA) ==> 0
// 1秒后更新 count.value = 1
count.value1 (fnB) ==> 1
// 🚨 依赖丢失:外层 effect (fnA) 竟没有被触发!
核心问题: 为什么外层 effect 能够被首次执行,但无法在数据更新时被触发?
🔬 问题分析
响应式系统依赖全局变量 activeSub 来标记当前的副作用函数。在嵌套场景中,内层 effect 的清理行为干扰了外层的依赖收集。
| 触发操作 | activeSub状态 | 关键问题 |
|---|---|---|
外层fn启动 | ➡️ fnA | 标记 activeSub 为外层 |
内层fn启动 | ➡️ fnB | fnB 覆盖 fnA |
fnB 读取 count.value | ➡️ fnB | ✅ 依赖收集成功:count 依赖 fnB。 |
内层fnB结束 | ➡️ undefined | ❌ fnB 清空了 activeSub。 |
fnA 继续执行, 读取 count.value | ➡️ undefined | ❌ 依赖收集失败:系统认为没有 effect 活动。 |
根本原因: 内层 effect 结束后,将全局变量 activeSub 暴力重置为 undefined,导致外层 effect 的上下文信息丢失,无法完成后续的依赖收集。
✨ 优雅的解决方案
解决方案是利用函数的调用栈特性,在进入内层 effect 时暂存外层的 activeSub,并在内层 effect 结束后将其恢复。
🛠️ ReactiveEffect 类的改进
export let activeSub: ReactiveEffect | undefined
export class ReactiveEffect {
// ... constructor
run(): any {
// 1. 🔑 关键改进:保存当前的 effect 上下文
const prevSub = activeSub
// 2. 设置当前活动的 effect (进入内层)
activeSub = this
try {
// 3. 执行副作用函数 (内层执行完毕)
return this.fn()
}
finally {
// 4. ✅ 恢复:无论执行是否成功,都恢复之前保存的外层 effect
activeSub = prevSub
}
}
}
通过这种上下文堆栈式的保存和恢复机制,我们确保了在任何嵌套深度下,每个 effect 都能正确地完成依赖收集。