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

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


一、描述

在 Vue3 的响应式系统中,effect(副作用函数)会在其所依赖的响应式数据变化时重新执行。但如果副作用函数在执行过程中又修改了自身所依赖的响应式数据,就可能触发“递归执行”,甚至导致“无限循环”。Vue3 内部通过一套机制来检测并避免这种情况,保证系统的稳定性。


二、核心概念理解

  1. effect 递归执行
    effect 执行时,如果它内部修改了某个响应式数据,而这个数据恰好是该 effect 的依赖,则会再次触发同一个 effect 执行,形成递归调用。

  2. 无限循环风险
    若递归执行没有终止条件,就会导致无限循环,例如:

    const obj = reactive({ count: 0 })
    effect(() => {
      obj.count++ // 每次执行都修改依赖,触发自身再次执行
    })
    
  3. 检测机制目标
    在保证响应式正确性的前提下,避免无限递归导致的栈溢出或死循环。


三、源码级实现机制

步骤1:effect 的执行流程与标记

Vue3 的 effect 函数(位于 packages/reactivity/src/effect.ts)内部通过一个 activeEffect 全局变量来追踪当前正在执行的副作用。执行前会将其推入“执行栈”,执行后弹出。

关键代码结构简化:

let activeEffect: ReactiveEffect | undefined
let effectStack: ReactiveEffect[] = [] // 执行栈

function run(effect: ReactiveEffect) {
  if (!effect.active) return
  // 防止递归:检查当前 effect 是否已在栈中
  if (effectStack.includes(effect)) return
  
  // 清理旧依赖(稍后详述)
  cleanup(effect)
  
  // 压栈并执行
  effectStack.push(effect)
  activeEffect = effect
  try {
    return effect.fn() // 执行用户传入的函数
  } finally {
    // 出栈
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
}
  • 执行栈 effectStack:记录正在执行的 effect 链,用于检测嵌套。
  • activeEffect:始终指向栈顶的 effect,用于依赖收集时绑定。

步骤2:递归执行的触发条件

effect.fn() 执行时,如果内部访问了响应式数据,会触发 track(依赖收集);如果修改了响应式数据,会触发 trigger(触发更新)。

关键点
trigger 函数中,会从依赖集合中取出所有相关的 effect,并加入待执行队列。如果待执行的 effect 与当前正在执行的 effect 相同,且没有防护机制,就会立即执行,导致递归。


步骤3:无限循环检测的核心逻辑

Vue3 通过两个机制防止无限递归:

机制一:执行栈去重
如上 run 函数中的 effectStack.includes(effect) 检查,如果当前 effect 已经在栈中,说明正在执行中,直接跳过本次执行。但这只能防止同步递归(即同一个 effect 在栈中出现两次)。

机制二:调度器(scheduler)与异步队列
Vue3 默认将 effect 的再次执行放入一个微任务队列(通过 scheduler),避免同步递归。

trigger 函数中:

export function trigger(target, type, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const effects = new Set<ReactiveEffect>()
  // 收集所有需要触发的 effect
  addEffects(effects, depsMap, key)
  
  const run = (effect: ReactiveEffect) => {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (effect.scheduler) {
        effect.scheduler() // 调度器执行,通常是放入队列
      } else {
        effect.run() // 直接执行
      }
    }
  }
  
  effects.forEach(run)
}
  • effect !== activeEffect:如果要触发的 effect 正是当前正在执行的 activeEffect,则跳过同步执行(除非显式设置 allowRecurse: true)。
  • 调度器(scheduler)effect 可以配置 scheduler,Vue3 的组件更新 effect 默认使用调度器将更新推入队列,异步执行,从而打破同步递归链。

步骤4:递归深度限制(安全兜底)

Vue3 还通过“最大递归深度”来防止极端情况下的无限循环。在开发模式下,如果检测到连续触发同一个 effect 超过一定次数(如 100 次),会抛出警告并停止执行。

简化示意:

let recursiveCount = 0
const MAX_RECURSION_COUNT = 100

function run(effect) {
  recursiveCount++
  if (recursiveCount > MAX_RECURSION_COUNT) {
    console.warn('Maximum recursive updates exceeded.')
    return
  }
  // ...正常执行逻辑
  recursiveCount--
}

四、实际场景示例

场景1:直接自增导致的递归

const obj = reactive({ count: 0 })
effect(() => {
  obj.count++ // 读取 count(track),然后设置 count(trigger)
})
  • 第一次执行 effect:读取 obj.count(触发 track,收集依赖)。
  • 执行 obj.count++:修改 obj.count(触发 trigger),由于 activeEffect 正是当前 effect,且无 schedulertrigger 中的 effect !== activeEffect 为 false,所以不会同步执行,避免了递归。
  • 但如果 effect 配置了 scheduler,则会调度到下一轮,不会栈溢出。

场景2:嵌套 effect 与间接递归

const a = reactive({ x: 0 })
const b = reactive({ y: 0 })

effect(() => {
  a.x = b.y + 1
})

effect(() => {
  b.y = a.x + 1
})
  • 执行 effect1:修改 a.x,触发 effect2。
  • 执行 effect2:修改 b.y,触发 effect1。
  • 形成交替递归,但通过执行栈检测调度器异步化,不会无限循环,而是执行一轮后停止(因为值已稳定)。

五、总结与设计思想

  1. 执行栈检测:防止同一 effect 在调用栈中重复出现(同步递归)。
  2. 调度器异步化:默认将 effect 的再次执行推迟到微任务队列,打破同步递归链。
  3. 递归深度兜底:开发环境下限制最大递归次数,避免死循环。
  4. allowRecurse 选项:允许显式控制是否允许递归,用于需要递归的特殊场景(极少使用)。

通过这些机制,Vue3 在保持响应式系统灵活性的同时,确保了执行过程的稳定性和安全性。

Vue3 的响应式系统源码级 effect 的递归执行与无限循环检测机制原理 一、描述 在 Vue3 的响应式系统中, effect (副作用函数)会在其所依赖的响应式数据变化时重新执行。但如果副作用函数在执行过程中又修改了自身所依赖的响应式数据,就可能触发“递归执行”,甚至导致“无限循环”。Vue3 内部通过一套机制来检测并避免这种情况,保证系统的稳定性。 二、核心概念理解 effect 递归执行 当 effect 执行时,如果它内部修改了某个响应式数据,而这个数据恰好是该 effect 的依赖,则会再次触发同一个 effect 执行,形成递归调用。 无限循环风险 若递归执行没有终止条件,就会导致无限循环,例如: 检测机制目标 在保证响应式正确性的前提下,避免无限递归导致的栈溢出或死循环。 三、源码级实现机制 步骤1:effect 的执行流程与标记 Vue3 的 effect 函数(位于 packages/reactivity/src/effect.ts )内部通过一个 activeEffect 全局变量来追踪当前正在执行的副作用。执行前会将其推入“执行栈”,执行后弹出。 关键代码结构简化: 执行栈 effectStack :记录正在执行的 effect 链,用于检测嵌套。 activeEffect :始终指向栈顶的 effect,用于依赖收集时绑定。 步骤2:递归执行的触发条件 当 effect.fn() 执行时,如果内部访问了响应式数据,会触发 track (依赖收集);如果修改了响应式数据,会触发 trigger (触发更新)。 关键点 : 在 trigger 函数中,会从依赖集合中取出所有相关的 effect ,并加入待执行队列。如果待执行的 effect 与当前正在执行的 effect 相同,且没有防护机制,就会立即执行,导致递归。 步骤3:无限循环检测的核心逻辑 Vue3 通过两个机制防止无限递归: 机制一:执行栈去重 如上 run 函数中的 effectStack.includes(effect) 检查,如果当前 effect 已经在栈中,说明正在执行中,直接跳过本次执行。但这只能防止 同步递归 (即同一个 effect 在栈中出现两次)。 机制二:调度器(scheduler)与异步队列 Vue3 默认将 effect 的再次执行放入一个微任务队列(通过 scheduler ),避免同步递归。 在 trigger 函数中: effect !== activeEffect :如果要触发的 effect 正是当前正在执行的 activeEffect,则跳过 同步执行 (除非显式设置 allowRecurse: true )。 调度器(scheduler) : effect 可以配置 scheduler ,Vue3 的组件更新 effect 默认使用调度器将更新推入队列,异步执行,从而打破同步递归链。 步骤4:递归深度限制(安全兜底) Vue3 还通过“最大递归深度”来防止极端情况下的无限循环。在开发模式下,如果检测到连续触发同一个 effect 超过一定次数(如 100 次),会抛出警告并停止执行。 简化示意: 四、实际场景示例 场景1:直接自增导致的递归 第一次执行 effect :读取 obj.count (触发 track ,收集依赖)。 执行 obj.count++ :修改 obj.count (触发 trigger ),由于 activeEffect 正是当前 effect,且无 scheduler , trigger 中的 effect !== activeEffect 为 false,所以 不会同步执行 ,避免了递归。 但如果 effect 配置了 scheduler ,则会调度到下一轮,不会栈溢出。 场景2:嵌套 effect 与间接递归 执行 effect1:修改 a.x ,触发 effect2。 执行 effect2:修改 b.y ,触发 effect1。 形成交替递归,但通过 执行栈检测 和 调度器异步化 ,不会无限循环,而是执行一轮后停止(因为值已稳定)。 五、总结与设计思想 执行栈检测 :防止同一 effect 在调用栈中重复出现(同步递归)。 调度器异步化 :默认将 effect 的再次执行推迟到微任务队列,打破同步递归链。 递归深度兜底 :开发环境下限制最大递归次数,避免死循环。 allowRecurse 选项 :允许显式控制是否允许递归,用于需要递归的特殊场景(极少使用)。 通过这些机制,Vue3 在保持响应式系统灵活性的同时,确保了执行过程的稳定性和安全性。