引言
effect 副作用函数是可以发生嵌套的,至于为什么要设计成这样呢
嵌套的 effect
effect(function effectFn1() {
effect(function effectFn2() { /* ... */ })
/* ... */
})
在上面这段代码中,effectFn1 内部嵌套了 effectFn2,effectFn1 的执行会导致 effectFn2 的执行。那么,什么场景下会出现嵌套的 effect 呢?拿 Vue.js 来说,实际上 Vue.js 的渲染函数就是在一个 effect 中执行的.
当组件发生嵌套时,例如 Foo 组件渲染了 Bar 组件:
// Bar 组件
const Bar = {
render() {/* ... */ },
}
// Foo 组件渲染了 Bar 组件
const Foo = {
render() {
return <Bar /> }// jsx 语法
}
此时就发生了 effect 嵌套,它相当于:
effect(() => {
Foo.render()
// 嵌套
effect(() => {
Bar.render()}
)}
)
effect函数可以嵌套使用,也就是说,一个effect函数内部可以包含另一个effect函数。当外部effect函数依赖于内部effect函数创建的响应式数据时,内部effect函数会被自动追踪,确保外部effect函数在内部effect函数发生变化时得以执行。
这种嵌套的effect函数用于创建依赖关系链,确保当某个响应式数据变化时,所有依赖于它的effect函数都能够被触发执行,从而保持应用的响应性。
而”effect 栈”,在Vue 3的内部实现中,Vue使用了一个effect栈来追踪当前正在执行的effect函数,这个栈的作用类似于函数调用栈,用于管理effect函数的执行顺序和依赖关系。
现在有一个不使用栈结构的嵌套的effect
函数的例子,但是他并不能实现嵌套的功能。假设我们有两个响应式数据count1
和count2
,其中count2
的值依赖于count1
的值。我们可以使用嵌套的effect
函数来实现这种依赖关系。
// 原始数据
const data = { foo: true, bar: true };
// 代理对象
const obj = new Proxy(data, {
get(target, key) {
console.log(`读取属性: ${key}`);
return target[key];
}
});
// 全局变量
let temp1, temp2;
// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {
console.log('effectFn1 执行');
effect(function effectFn2() {
console.log('effectFn2 执行');
// 在 effectFn2 中读取 obj.bar 属性
temp2 = obj.bar;
});
// 在 effectFn1 中读取 obj.foo 属性
temp1 = obj.foo;
});
effectFn1
是外部的effect
函数,它依赖于obj.foo
的值,并且在内部包含了一个innerEffect
,内部的effect
函数依赖于obj.bar
的值。当我们修改obj.foo
时,我们希望外部的effect
函数被触发执行,并且输出obj.foo
的值,然后触发内部的依赖函数。当我们修改obj.bar
时,内部的effect
函数被触发执行,并且输出obj.bar
的值。
我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;
function effect(fn) {
// 定义副作用函数
const effectFn = () => {
// 调用 cleanup 函数,具体实现需要根据需求补充
cleanup(effectFn);
// 将副作用函数赋值给 activeEffect
activeEffect = effectFn;
// 执行副作用函数
fn();
// 将当前副作用函数的依赖集合存储在 effectFn.deps 中(需要根据实际逻辑补充)
effectFn.deps = []; // 这里需要根据实际逻辑设置依赖集合
};
// 执行副作用函数
effectFn();
}
但其实只使用一个变量储存而不使用栈结构,当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,也就是说我在读取obj.foo
的时候,activeEffect还只是innerEffect的值,并且只触发了innerEffect的效果。
为了解决这个问题,我们需要一个副作用函数栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况:
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;
// effect 栈
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn); // 调用 cleanup 函数,具体实现需要根据需求补充
activeEffect = effectFn;
// 将当前副作用函数压入栈中
effectStack.push(effectFn);
fn();
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
// 初始化副作用函数的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
評論