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启动➡️ fnBfnB 覆盖 fnA
fnB 读取 count.value➡️ fnB✅ 依赖收集成功:count 依赖 fnB
内层fnB结束➡️ undefinedfnB 清空了 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 都能正确地完成依赖收集。