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
})
执行步骤分析:
effect首次执行,读取obj.count(触发track收集依赖)。- 执行
obj.count = obj.count + 1,触发trigger重新运行当前effect。 - 重新运行
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(达到递归限制后停止)
执行过程:
- 第一次执行,输出 0,触发
trigger。 - 第二次执行,输出 1,再次触发
trigger。 - 重复直到达到最大递归深度限制,浏览器会抛出错误。
7. 总结机制
Vue3 通过以下方式避免无限循环:
- 调用栈检测:同一个
effect不会在栈中重复执行。 - 递归深度限制:JavaScript 引擎自身会限制调用栈深度(例如 10000 层),超过会报错。
- 调度队列:异步更新队列可以合并重复的
effect执行。