问题
如果一个副作用函数中,既涉及到响应式对象的读操作,又涉及到响应式对象的写操作,就会出现无限递归循环的问题。
js
const data = {
value: 1,
};
const obj = new Proxy(data, ...)
effect(() => obj.value = obj.value + 1)
上面的代码中,副作用函数既有响应式对象的读操作,又有响应式对象的写操作。 完整代码如下:
js
// 原始对象,包含两个属性
const data = {
value: 1,
};
// 存放副作用函数的集合容器。之所以是用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new WeakMap();
// 表示当前正在运行的副作用函数
let activeEffect = null;
// 副作用栈
let effectStack = [];
// 用于执行副作用函数的函数
function effect(fn) {
const effectFn = () => {
// 清除依赖
cleanup(effectFn);
// 执行副作用函数
activeEffect = effectFn;
effectStack.push(activeEffect)
fn();
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
};
// 存储该副作用哦函数相关联的依赖
effectFn.deps = []
effectFn();
}
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
if(!activeEffect) return target[key]
track(target, key);
return target[key];
},
set(target, key, val) {
target[key] = val;
trigger(target, key);
return true;
},
});
function track(target, key){
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
activeEffect.deps.push(deps);
}
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun && effectsToRun.forEach(fn => fn())
}
////////////////////////////////////
effect(() => {
obj.value = obj.value + 1
});
// 输出控制台报错:
// RangeError: Maximum call stack size exceeded
分析
我们分析下为什么报超过内存栈容量的错误。
- 当运行effect方法,触发obj.value的读操作,从而触发track方法。
- 在track方法中,当前副作用函数会被添加到obj.value对应的deps中;
- 有由于effect中又涉及到obj.value的写操作,从而触发了trigger方法。
- trigger方法会从deps中取出所有的副作用函数并执行。
- 在第4步中执行所有副作用函数,一定会包含当前正在运行的那个副作用函数。因此也就出现了当前的函数运行中,又调用了自己的情况。从而会返回第1步的流程,进入了无限循环。
解决上面的问题思路很简单,在trigger函数中,如果发现当前deps将要执行的函数和当前正在执行的函数是同一个,那么就跳过不执行它。
js
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
// const effectsToRun = new Set(effects)
const effectToRun = new Set()
effects && effects.forEach(effectFn => {
// 当前的副作用函数和activeEffect不一样,才会添加到执行集合中
if(effectFn !== activeEffect) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(fn => fn())
}
总结
如果一个副作用函数中,既涉及到响应式对象的读操作,又涉及到响应式对象的写操作,就会出现无限递归循环的问题。解决的思路就是在trigge函数中,如果发现将要执行的副作用函数和当前正在执行的副作用函数是同一个的时候,跳过不执行这个函数。
代码
参考
- 《Vue设计与实现》,作者:霍春阳,ISBN: 9787115583864