4.3嵌套的effect与effect栈
1、嵌套的effect
effect是可以发生嵌套的
01 effect(function effectFn1() {
02 effect(function effectFn2() { /* ... */ })
03 /* ... */
04 })
有这么一段代码:
01 // 原始数据
02 const data = { foo: true, bar: true }
03 // 代理对象
04 const obj = new Proxy(data, { /* ... */ })
05
06 // 全局变量
07 let temp1, temp2
08
09 // effectFn1 嵌套了 effectFn2
10 effect(function effectFn1() {
11 console.log('effectFn1 执行')
12
13 effect(function effectFn2() {
14 console.log('effectFn2 执行')
15 // 在 effectFn2 中读取 obj.bar 属性
16 temp2 = obj.bar
17 })
18 // 在 effectFn1 中读取 obj.foo 属性
19 temp1 = obj.foo
20 })
我们希望副作用函数与对象属性之间的关系如下:
01 data
02 └── foo
03 └── effectFn1
04 └── bar
05 └── effectFn2
但是当我们发现我们修改foo的值出来的却是:
01 'effectFn1 执行'
02 'effectFn2 执行'
03 'effectFn2 执行'
一共打印三次,前两次分别是副作用函数effectFn1 与 effectFn2 初始执行的打印结果,到这一步是正常的,问题出在第三行打印。我们修改了字段 obj.foo 的值,发现 effectFn1 并没有重新执行,反而使得 effectFn2 重新执行了,这显然不符合预期。
问题出现在哪里呢?
01 // 用一个全局变量存储当前激活的 effect 函数
02 let activeEffect
03 function effect(fn) {
04 const effectFn = () => {
05 cleanup(effectFn)
06 // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
07 activeEffect = effectFn
08 fn()
09 }
10 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
11 effectFn.deps = []
12 // 执行副作用函数
13 effectFn()
14 }
我们用全局变量 activeEffect 来存储通过 effect函数注册的副作用函数,这意味着同一时刻activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值,那么foo应该放入的是effect1也会被effect2所覆盖。
2、effect栈
如何解决???effect栈
在副作用函数执行时,将当前副作 用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况。
修改代码:
let activeEffect;
// effect栈
const effectStack = [];
function effect (fn) {
const effectFn = () => {
// 调用clearup函数完成清除工作
clearUp(effectFn);
activeEffect = effectFn;
// 在调用副作用函数之前将副作用函数压入栈中
effectStack.push(effectFn);
fn();
// 执行完之后抛出,把activeEffect还原成原来的值
effectStack.pop();
//effectStack[effectStack.length - 1] 用于获取栈顶的副作用函数,并将其设置为 activeEffect。
activeEffect = effectStack[effectStack.length - 1];
}
// deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
完整代码:
<script setup>
let activeEffect;
// effect栈
const effectStack = [];
function effect (fn) {
const effectFn = () => {
// 调用clearup函数完成清除工作
clearUp(effectFn);
activeEffect = effectFn;
// 在调用副作用函数之前将副作用函数压入栈中
effectStack.push(effectFn);
fn();
// 执行完之后抛出,把activeEffect还原成原来的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
// deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
const bucket = new WeakMap();
const data = { text: 'world' }; // 确保所有属性都已定义
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);
effectsToRun.forEach(effectFn => 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('run');
document.body.innerText = obj.text;
});
setTimeout(() => {
obj.text = "hello vue3"; // 修改已定义的属性以触发依赖
}, 1000);
</script>
由图所示:那么外层的副作用函数就会压到栈底,内层就在栈顶
[effect2,effect1]=> [effect1]=>[]:修改foo输出
01 'effectFn1 执行'
02 'effectFn2 执行'
03 'effectFn2 执行'
04 'effectFn1 执行'
与键相关的副作用函数
}
// 清除函数
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(‘run’);
document.body.innerText = obj.text;
});
setTimeout(() => {
obj.text = “hello vue3”; // 修改已定义的属性以触发依赖
}, 1000);
[外链图片转存中...(img-84C5fgLt-1747235972579)]
由图所示:那么外层的副作用函数就会压到栈底,内层就在栈顶
[effect2,effect1]=> [effect1]=>[]:修改foo输出
01 ‘effectFn1 执行’
02 ‘effectFn2 执行’
03 ‘effectFn2 执行’
04 ‘effectFn1 执行’