前言
没想到响应式系统系列已经更到第五篇文章了(鼓掌👏),一开始只是打算随便写写,给自己练练手。虽然现在也没啥阅读量,但没打算放弃,主要用来巩固知识点。
在上篇文章中我们考虑了effect 嵌套的问题,并使用effectStack 栈来处理嵌套。传送门
但是想要设计一个完善的响应式系统,还需要考虑诸多其他细节,本篇文章要介绍的无限递归循环也是其中之一。
正文
举例
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
obj.foo++
})
可能没有了解过响应式系统的同学对上面代码会有些疑惑,这里简单介绍一下:Proxy 返回的obj 对象可以简单看作Vue3 中的一个响应式对象reactive。effect 函数用来收集副作用函数,当副作用函数中的响应式数据obj.foo 发生变化时,此副作用函数将会被再次执行。想了解更多的话,可以看看 Vue3 设计响应式系统第一篇。
在上面代码中,使用obj 代理了data 对象,并在effect 函数中注册了一个副作用函数用户自增obj.foo。当我们运行代码时,系统提示我们栈溢出:
为什么会这样呢?
我们可以把obj.foo++分开来看,它相当于obj.foo = obj.foo + 1,也就是说在副作用函数中,即存在对响应式的读取,同时存在对响应式数据的设置。
我们来分析一下: 当响应式数据发生变化的时候,会触发副作用函数的执行,而副作用函数的执行又会使响应式数据变化,响应式数据变化又又触发副作用函数执行... 从而陷入了无限递归循环中。
解决方案
想要解决这个问题其实也很简单,在解答之前,建议花五分钟回顾一下Vue3 设计响应式系统(二)。这篇文章中,我们定义了activeEffect 变量来存储当前正处于激活状态的副作用函数,并根据activeEffect 来收集副作用函数依赖。
观察上面例子中的代码,我们能发现读取和设置操作是在同一个副作用函数中执行的。也就是说,此时无论是track 收集的副作用函数,还是trigger 时要触发执行的副作用函数,都是activeEffect。基于此,我们可以在trigger 函数中添加守卫条件:如果trigger 触发执行的副作用函数与当前正在执行的副作用函数(即activeEffect)相同,则不触发执行。代码如下:
// bucket 用于存储响应式数据与副作用函数的依赖集
// 收集过程发生在track 函数中
const bucket = new WeakMap()
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const deps = depsMap.get(key)
const effectsToRun = new Set()
// 如果trigger 函数触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
effects && effects.forEach((effectFn) => {
if(effectFn !== activeEffect) effectsToRun.add(effectFn)
})
effectsToRun && effectsToRun.forEach((effectFn) => effectFn())
}
这样我们就能够避免无限递归调用,从而避免栈溢出。
总结
在这篇文章中,我们分析了无限递归循环的情况。并通过观察得知出现这种情况的原因,即track 收集的副作用函数和trigger 时要触发执行的副作用函数,都是activeEffect。由此引入了解决方法,在trigger 函数中添加守卫条件:如果trigger 函数触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行,从而避免了无限递归调用。