如前文所说,实现一个完善的响应系统要考虑诸多细节。而本节 要介绍的无限递归循环就是其中之一,还是举个例子:
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => obj.foo++)
可以看到,在 effect 注册的副作用函数内有一个自增操作 obj.foo++ ,该操作会引起栈溢出:
Uncaught RangeError: Maximum call stack size exceeded
为什么会这样呢?接下来我们就尝试搞清楚这个问题,并提供解 决方案。 实际上,我们可以把 obj.foo++ 这个自增操作分开来看,它相 当于:
effect(() => {
// 语句
obj.foo = obj.foo + 1 04
})
在这个语句中,既会读取 obj.foo 的值,又会设置 obj.foo 的 值,而这就是导致问题的根本原因。我们可以尝试推理一下代码的执 行流程:首先读取 obj.foo 的值,这会触发 track 操作,将当前副 作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会 触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是 该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执 行。这样会导致无限递归地调用自己,于是就产生了栈溢出。 解决办法并不难。通过分析这个问题我们能够发现,读取和设置 操作是在同一个副作用函数内进行的。此时无论是 track 时收集的副 作用函数,还是 trigger 时要触发执行的副作用函数,都是 activeEffect。基于此,我们可以在 trigger 动作发生时增加守 卫条件:如果 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) => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则
不触发执行
if (effectFn !== activeEffect) {
// 新增
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach((effectFn) => effectFn())
// effects && effects.forEach(effectFn => effectFn())
}
这样我们就能够避免无限递归调用,从而避免栈溢出。