4.6 避免无限递归循环
obj.foo++在同一个副作用函数内既读又写,会自己触发自己直至栈溢出;在trigger中加一条守卫条件即可。
一、问题复现
effect(() => obj.foo++) // 报错:Maximum call stack size exceeded
执行过程:
- 读取
obj.foo→ 触发track,把当前副作用函数收集进「桶」; - 自增后再赋值给
obj.foo→ 触发trigger,把桶里的副作用函数取出执行; - 而此刻这个副作用函数还没执行完就开始了下一次执行 → 无限递归。
obj.foo++等价于obj.foo = obj.foo + 1,天然同时包含读取与设置,是最容易踩到的写法。
二、问题的本质
读取与设置发生在同一个副作用函数内:
track时收集的副作用函数是activeEffect;trigger时准备触发的副作用函数也是activeEffect。
两者是同一个函数,所以才会自己调自己。
三、解决办法:增加守卫条件
在 trigger 中,若要执行的副作用函数与当前正在执行的副作用函数相同,则跳过:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 守卫:自己触发自己不执行,避免无限递归
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
}
这条守卫正好与「遍历需要副本」的需求合并:
effectsToRun既完成过滤,又充当了Set副本。
四、易错点
守卫比较的是函数引用,因此参与比较的必须是
effect内部包装出来的effectFn,而不是用户传入的fn。 该守卫只拦「同一个函数」,其它副作用函数照常被触发。
相关
- 4.5 嵌套的 effect 与 effect 栈:
activeEffect与 effect 栈 - 4.7 调度执行:既然能决定「执不执行」,就能决定「什么时候执行」