Vue响应式原理(5)-解决自增操作导致的无限递归循环问题

158 阅读2分钟

1. 无限递归循环问题

# Vue响应式原理(4)-无限循环和effect嵌套问题解决一文中,我们对响应式系统存在的两个问题进行了解决,到目前为止我们实现的响应式系统已经能提供相对较为完善的功能,但是仍然存在一些问题。我们可以运行如下代码:

const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => obj.foo++)

effect 注册的副作用函数中,我们对 obj.foo 进行自增操作,但是运行结果显示栈溢出:

Uncaught RangeError: Maximum call stack size exceeded

2. 问题原因分析

effect(() => obj.foo++)

在副作用函数中的 obj.foo 自增操作实际相当于执行以下代码:

effect(() => obj.foo = obj.foo+1)

我们可以对代码的执行流程逐步分析:首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。问题的原因就在于此时该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

3. 问题解决

问题产生的原因总结来说就是因为当前正在执行的副作用函数和trigger触发依赖的副作用函数是同一个函数,导致了递归调用自身且没有终止条件。容易想到解决方案就是在trigger中进行判断,判断当前执行的副作用函数和从deps中取出的副作用函数是否是同一个函数,如果是同一个函数则不予执行。对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())
}