Vue.js---避免无限递归循环 调度执行
4.4 避免无限递归循环
什么情况下会无限递归?
01 const data = { foo: 1 }
02 const obj = new Proxy(data, { /*...*/ })
03
04 effect(() => obj.foo++)
例如这种情况,它会反复设置添加一直到栈溢出
首先读取obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。
如何解决?
因为读取和设置操作是在同一个副作用函数内进行的。如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
// 开始执行const effectsToRun = new Set(effects);effects && effects.forEach(effectFn => {if(activeEffect !== effectFn){effectsToRun.add(effectFn);}});
4.5 调度执行
什么是可调度性:当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
01 const data = { foo: 1 }
02 const obj = new Proxy(data, { /* ... */ })
03
04 effect(() => {
05 console.log(obj.foo)
06 })
07
08 obj.foo++
09
10 console.log('结束了')
这一段代码输出:
1
2
'结束了'
假如现在需求有变,我们想要它输出:
1
'结束了'
2
这时候我们想要一种可以不改变代码的位置,就可以实现这种效果的方式---->调度执行
我们可以为 effect 函数设计一个选项参数options,允许用户指定调度器:
function effect (fn , options = {}) {const effectFn = () => {// 调用clearup函数完成清除工作clearUp(effectFn);activeEffect = effectFn; // 在调用副作用函数之前将副作用函数压入栈中effectStack.push(effectFn);fn();// 执行完之后抛出,把activeEffect还原成原来的值effectStack.pop();activeEffect = effectStack[effectStack.length - 1];}// 将options挂在到fn上effectFn.options = options;// deps用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = [];// 执行副作用函数effectFn();}
trigger函数当中,在副作用函数执行之前,根据调度内容进行调度
// 触发变化:处理调度逻辑function trigger(target, key, newVal) {const depsMap = bucket.get(target);if (!depsMap) {return;}const effects = depsMap.get(key);// 开始执行const effectsToRun = new Set(effects);effectsToRun.forEach(effectFn =>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn);}else {effectFn();}});// effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数}
连续多次修改只进行最后一次的更新:
// 连续执行同一代码,只饭后最后一次计算的结果// 定义一个任务集合set:可以进行去重操作const jobQueue = new Set();// 使用promise.resolve()创建一个promise实例:将一个任务添加到微任务队列当中const p = Promise.resolve();// 开始刷新队列let isFlushing = false;function flushJob(){// 如果队列正在被刷新->returnif(isFlushing){return;}// 没有刷新,进行刷新操作isFlushing = true;p.then(() => {jobQueue.forEach(job => job());}).finally(() => {// 结束后重置isFlushing = false;})}effect(() => {console.log(obj.foo);} , {scheduler(fn){jobQueue.add(fn);//调用flushJob刷新队列flushJob();}})obj.foo++;obj.foo++;
目前的响应代码:
<script setup>let activeEffect;// effect栈const effectStack = [];function effect (fn , options = {}) {const effectFn = () => {// 调用clearup函数完成清除工作clearUp(effectFn);activeEffect = effectFn; // 在调用副作用函数之前将副作用函数压入栈中effectStack.push(effectFn);fn();// 执行完之后抛出,把activeEffect还原成原来的值effectStack.pop();activeEffect = effectStack[effectStack.length - 1];}// 将options挂在到fn上effectFn.options = options;// deps用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = [];// 执行副作用函数effectFn();}const bucket = new WeakMap();const data = { foo : 1 }; // 确保所有属性都已定义const obj = new Proxy(data, {get(target, key){track(target , key);return target[key];},set(target, key, newVal){target[key] = newVal;trigger(target , key , newVal);}});// 追踪变化function track(target , key){if(!activeEffect){return;}// 根据tartget取来的depsMap,它是一个map类型let depsMap = bucket.get(target);// 如果不存在if(!depsMap){// 创建一个bucket.set(target, (depsMap = new Map()));}// 根据key取来的deps,它是一个set类型let deps = depsMap.get(key);// 如果不存在if(!deps){// 创建一个depsMap.set(key, (deps = new Set()));}deps.add(activeEffect); // 添加当前活跃的副作用函数activeEffect.deps.push(deps);}
// 触发变化:处理调度逻辑function trigger(target, key, newVal) {const depsMap = bucket.get(target);if (!depsMap) {return;}const effects = depsMap.get(key);// 开始执行const effectsToRun = new Set(effects);effects && effects.forEach(effectFn => {if(activeEffect !== effectFn){effectsToRun.add(effectFn);}});effectsToRun.forEach(effectFn =>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn);}else {effectFn();}});// effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数}// 清除函数function clearUp (effectFn){// 遍历然后进行删除for(let i = 0 ; i < effectFn.deps.length ; i++){const deps = effectFn.deps[i];// 移除deps.delete(effectFn);}// 最后重置effectFn.deps数组effectFn.deps.length = 0;}// effect(() => {// console.log(obj.foo);// })// obj.foo ++;// obj.foo++;// 连续执行同一代码,只饭后最后一次计算的结果// 定义一个任务集合set:可以进行去重操作const jobQueue = new Set();// 使用promise.resolve()创建一个promise实例:将一个任务添加到微任务队列当中const p = Promise.resolve();// 开始刷新队列let isFlushing = false;function flushJob(){// 如果队列正在被刷新->returnif(isFlushing){return;}// 没有刷新,进行刷新操作isFlushing = true;p.then(() => {jobQueue.forEach(job => job());}).finally(() => {// 结束后重置isFlushing = false;})}effect(() => {console.log(obj.foo);} , {scheduler(fn){jobQueue.add(fn);//调用flushJob刷新队列flushJob();}})obj.foo++;obj.foo++;</script>
effectFn);
}
// 最后重置effectFn.deps数组
effectFn.deps.length = 0;
}
// effect(() => {
// console.log(obj.foo);
// })
// obj.foo ++;
// obj.foo++;
// 连续执行同一代码,只饭后最后一次计算的结果
// 定义一个任务集合set:可以进行去重操作
const jobQueue = new Set();
// 使用promise.resolve()创建一个promise实例:将一个任务添加到微任务队列当中
const p = Promise.resolve();
// 开始刷新队列
let isFlushing = false;
function flushJob(){
// 如果队列正在被刷新->return
if(isFlushing){
return;
}
// 没有刷新,进行刷新操作
isFlushing = true;
p.then(() => {
jobQueue.forEach(job => job());
}).finally(() => {// 结束后重置isFlushing = false;})
}effect(() => {console.log(obj.foo);
} , {scheduler(fn){jobQueue.add(fn);//调用flushJob刷新队列flushJob();}
})
obj.foo++;
obj.foo++;
```