通过保存并恢复 activeSub,解决嵌套 effect 中外层依赖丢失的问题
上一篇把 effect 包装成了 ReactiveEffect,并把依赖收集和派发更新拆到了 trackRef、triggerRef 和 system.ts 里。
还是熟悉的场景:嵌套 effect。
嵌套 effect 的问题
先增加一个测试用例:
it('嵌套的 effect', () => {
const count = ref(0)
let runCount = 0
effect(() => {
effect(() => {
count.value
runCount++
})
count.value
runCount++
})
count.value = 1
expect(runCount).toBe(4)
})
默认执行时,内层 effect 和外层 effect 都会读取 count.value。
所以第一次注册时会执行两次:
- 内层
effect执行一次,runCount变成1 - 外层
effect执行一次,runCount变成2
当 count.value = 1 后,按预期两个 effect 都应该重新执行,所以 runCount 至少应该变成 4。
但当前实际只执行了三次。

关键原因是:count 的依赖链表上只有内层的 ReactiveEffect2,外层的 ReactiveEffect1 丢失了。
少执行一次的原因
当前 ReactiveEffect.run 的逻辑:
run() {
activeSub = this
try {
return this.fn()
}
finally {
activeSub = undefined
}
}

执行嵌套 effect 时,会发生这样的过程:
- 执行外层
effect,此时activeSub = ReactiveEffect1 - 外层函数内部又执行了内层
effect - 执行内层
effect,此时activeSub = ReactiveEffect2 - 内层读取
count.value,收集到ReactiveEffect2 - 内层执行结束,把
activeSub重置为undefined - 回到外层继续执行,外层读取
count.value - 但此时
activeSub已经是undefined,所以外层依赖没有被收集
也就是说,内层 effect 执行完以后,把外层正在使用的 activeSub 清掉了。
所以后面触发更新时,count 只知道自己依赖了内层 effect,不知道还有外层 effect。
解决方式
这个问题和之前“后面的订阅者覆盖前面的订阅者”很像,本质上都是当前状态被覆盖了。
这里可以把 activeSub 当成一个调用栈来处理:
- 进入当前
effect前,先保存上一个activeSub - 当前
effect执行时,把activeSub设置成自己 - 当前
effect执行完后,再恢复成上一个activeSub
export let activeSub: ReactiveEffect | undefined
export class ReactiveEffect {
constructor(public fn: Function) {
}
run() {
const prevSub = activeSub
activeSub = this
try {
return this.fn()
}
finally {
activeSub = prevSub
}
}
}
export function effect(fn: Function) {
const e = new ReactiveEffect(fn)
e.run()
}
这样内层 effect 执行结束后,不会直接把 activeSub 清空,而是恢复成外层的 ReactiveEffect1。
接着外层继续读取 count.value 时,就能正常收集到外层依赖。
更新后多执行一次
修复 activeSub 后,外层依赖确实不会丢了。
但这个测试还有一个细节:更新时会比预期多执行一次。
触发更新时,propagate 会遍历 count 的依赖链表。假设链表里有两个依赖:
ReactiveEffect2:内层effectReactiveEffect1:外层effect
更新过程大概是:
- 执行内层
ReactiveEffect2.run() - 内层函数执行,
runCount++ - 执行外层
ReactiveEffect1.run() - 外层函数内部会重新创建一个全新的内层
effect - 新的内层
effect会立即执行,runCount++ - 外层函数继续执行,
runCount++
所以外层 effect 每次重新执行时,都会重新创建一个新的内层 effect。
如果内层逻辑是网络请求、大数据量计算,或者会触发 DOM 重新布局,这种重复执行的成本就会很高。
这也是官方不推荐书写嵌套 effect 的原因。