Vue3 的响应式系统源码级 effect 的递归执行与无限循环检测机制原理
一、描述
在 Vue3 的响应式系统中,effect(副作用函数)会在其所依赖的响应式数据变化时重新执行。但如果副作用函数在执行过程中又修改了自身所依赖的响应式数据,就可能触发“递归执行”,甚至导致“无限循环”。Vue3 内部通过一套机制来检测并避免这种情况,保证系统的稳定性。
二、核心概念理解
-
effect 递归执行
当effect执行时,如果它内部修改了某个响应式数据,而这个数据恰好是该effect的依赖,则会再次触发同一个effect执行,形成递归调用。 -
无限循环风险
若递归执行没有终止条件,就会导致无限循环,例如:const obj = reactive({ count: 0 }) effect(() => { obj.count++ // 每次执行都修改依赖,触发自身再次执行 }) -
检测机制目标
在保证响应式正确性的前提下,避免无限递归导致的栈溢出或死循环。
三、源码级实现机制
步骤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,且无scheduler,trigger中的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。 - 形成交替递归,但通过执行栈检测和调度器异步化,不会无限循环,而是执行一轮后停止(因为值已稳定)。
五、总结与设计思想
- 执行栈检测:防止同一 effect 在调用栈中重复出现(同步递归)。
- 调度器异步化:默认将 effect 的再次执行推迟到微任务队列,打破同步递归链。
- 递归深度兜底:开发环境下限制最大递归次数,避免死循环。
allowRecurse选项:允许显式控制是否允许递归,用于需要递归的特殊场景(极少使用)。
通过这些机制,Vue3 在保持响应式系统灵活性的同时,确保了执行过程的稳定性和安全性。