Vue3 的响应式系统源码级 effect 的递归执行与无限循环检测机制
字数 1382 2025-12-06 13:20:13

Vue3 的响应式系统源码级 effect 的递归执行与无限循环检测机制

题目描述
在 Vue3 的响应式系统中,effect 函数用于创建响应式副作用。当响应式数据变化时,会触发 effect 重新执行。但如果 effect 内部修改了它依赖的响应式数据,就可能导致无限递归调用。Vue3 是如何检测和避免这种无限循环的?


解题过程循序渐进讲解

1. 基础场景回顾
首先,回忆一下 effect 的基本执行流程:

  • 当执行 effect(fn) 时,会创建一个 ReactiveEffect 实例,并立即执行 fn
  • 执行 fn 过程中,访问响应式数据会触发 track 进行依赖收集,将当前 effect 作为依赖存储。
  • 当响应式数据变化时,触发 trigger 执行所有收集到的依赖(即 effect)。

2. 无限循环的产生条件
考虑以下代码:

const obj = reactive({ count: 0 })

effect(() => {
  obj.count = obj.count + 1
})

执行步骤分析:

  1. effect 首次执行,读取 obj.count(触发 track 收集依赖)。
  2. 执行 obj.count = obj.count + 1,触发 trigger 重新运行当前 effect
  3. 重新运行 effect 又触发 trigger……形成无限循环。

3. Vue3 的检测机制
Vue3 在 ReactiveEffect.run 方法中通过 执行标记 来检测递归调用。核心代码(简化)如下:

class ReactiveEffect {
  run() {
    if (!effectStack.includes(this)) {
      try {
        effectStack.push(this)
        activeEffect = this
        return this.fn()
      } finally {
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
}

关键点:

  • effectStack 是一个全局数组,记录当前正在执行的 effect 调用栈。
  • 执行 effect 前,会检查当前 effect 是否已在栈中(effectStack.includes(this))。
  • 如果在栈中,则说明出现了递归调用,此时 不会重复执行 当前 effect

4. 递归调用的处理策略
实际上,Vue3 对递归调用的处理分为两种情况:

  • 直接递归:同一个 effect 在栈中再次被触发,则跳过执行,避免无限循环。
  • 间接递归:多个 effect 互相依赖形成循环,Vue3 通过 最大递归次数 来避免死循环(默认限制为 100 层)。

trigger 执行依赖时,Vue3 会先复制依赖数组,然后遍历执行。如果出现循环调用,调用栈深度会不断增加,但不会无限执行。

5. 与调度器(scheduler)的协同
如果 effect 配置了 scheduler,则触发更新时不会立即执行 effect,而是将任务推入队列。这也能避免部分递归问题,因为重复的 effect 会在队列中被去重。

6. 实际示例分析
修改之前的例子,看看实际执行过程:

const obj = reactive({ count: 0 })

effect(() => {
  console.log(obj.count)
  obj.count = obj.count + 1
})
// 输出:0, 1, 2, ..., 100(达到递归限制后停止)

执行过程:

  1. 第一次执行,输出 0,触发 trigger
  2. 第二次执行,输出 1,再次触发 trigger
  3. 重复直到达到最大递归深度限制,浏览器会抛出错误。

7. 总结机制
Vue3 通过以下方式避免无限循环:

  • 调用栈检测:同一个 effect 不会在栈中重复执行。
  • 递归深度限制:JavaScript 引擎自身会限制调用栈深度(例如 10000 层),超过会报错。
  • 调度队列:异步更新队列可以合并重复的 effect 执行。
Vue3 的响应式系统源码级 effect 的递归执行与无限循环检测机制 题目描述 : 在 Vue3 的响应式系统中, effect 函数用于创建响应式副作用。当响应式数据变化时,会触发 effect 重新执行。但如果 effect 内部修改了它依赖的响应式数据,就可能导致无限递归调用。Vue3 是如何检测和避免这种无限循环的? 解题过程循序渐进讲解 : 1. 基础场景回顾 首先,回忆一下 effect 的基本执行流程: 当执行 effect(fn) 时,会创建一个 ReactiveEffect 实例,并立即执行 fn 。 执行 fn 过程中,访问响应式数据会触发 track 进行依赖收集,将当前 effect 作为依赖存储。 当响应式数据变化时,触发 trigger 执行所有收集到的依赖(即 effect )。 2. 无限循环的产生条件 考虑以下代码: 执行步骤分析: effect 首次执行,读取 obj.count (触发 track 收集依赖)。 执行 obj.count = obj.count + 1 ,触发 trigger 重新运行当前 effect 。 重新运行 effect 又触发 trigger ……形成无限循环。 3. Vue3 的检测机制 Vue3 在 ReactiveEffect.run 方法中通过 执行标记 来检测递归调用。核心代码(简化)如下: 关键点: effectStack 是一个全局数组,记录当前正在执行的 effect 调用栈。 执行 effect 前,会检查当前 effect 是否已在栈中( effectStack.includes(this) )。 如果在栈中,则说明出现了递归调用,此时 不会重复执行 当前 effect 。 4. 递归调用的处理策略 实际上,Vue3 对递归调用的处理分为两种情况: 直接递归 :同一个 effect 在栈中再次被触发,则跳过执行,避免无限循环。 间接递归 :多个 effect 互相依赖形成循环,Vue3 通过 最大递归次数 来避免死循环(默认限制为 100 层)。 在 trigger 执行依赖时,Vue3 会先复制依赖数组,然后遍历执行。如果出现循环调用,调用栈深度会不断增加,但不会无限执行。 5. 与调度器(scheduler)的协同 如果 effect 配置了 scheduler ,则触发更新时不会立即执行 effect ,而是将任务推入队列。这也能避免部分递归问题,因为重复的 effect 会在队列中被去重。 6. 实际示例分析 修改之前的例子,看看实际执行过程: 执行过程: 第一次执行,输出 0,触发 trigger 。 第二次执行,输出 1,再次触发 trigger 。 重复直到达到最大递归深度限制,浏览器会抛出错误。 7. 总结机制 Vue3 通过以下方式避免无限循环: 调用栈检测 :同一个 effect 不会在栈中重复执行。 递归深度限制 :JavaScript 引擎自身会限制调用栈深度(例如 10000 层),超过会报错。 调度队列 :异步更新队列可以合并重复的 effect 执行。