Introduction
Effect functions can be nested. But why is this design choice made?
Nested Effects
effect(function effectFn1() {
effect(function effectFn2() { /* ... */ })
/* ... */
})
In the code above, effectFn1
is nested within effectFn2
. The execution of effectFn1
will trigger the execution of effectFn2
. So, when do we encounter nested effects? Take Vue.js, for example; Vue.js’s rendering function is executed within an effect.
When components are nested, for instance, when the Foo component renders the Bar component:
// Bar component
const Bar = {
render() {/* ... */ },
}
// Foo component renders the Bar component
const Foo = {
render() {
return <Bar /> }// JSX syntax
}
Nested effects occur in this scenario. It can be visualized as follows:
effect(() => {
Foo.render()
// Nested
effect(() => {
Bar.render()}
)}
)
The effect
function can be nested, meaning an effect
function can contain another effect
function. When an outer effect
function depends on reactive data created inside an inner effect
function, the inner effect
function is automatically tracked, ensuring that the outer effect
function is executed when the inner effect
function changes.
This nested structure of effect functions establishes a dependency chain, ensuring that when reactive data changes, all the effect functions dependent on it are triggered, thereby maintaining the responsiveness of the application.
The “effect stack” in Vue 3’s internal implementation is crucial. Vue uses an effect stack to track the currently executing effect functions, similar to a function call stack. This stack manages the execution order and dependency relationships of effect functions.
Now, let’s consider an example of a nested effect
function without using a stack structure. However, it cannot achieve the desired nesting functionality. Let’s assume we have two reactive data, count1
and count2
, where the value of count2
depends on the value of count1
. We can use nested effect
functions to establish this dependency relationship.
// Original data
const data = { foo: true, bar: true };
// Proxy object
const obj = new Proxy(data, {
get(target, key) {
console.log(`Reading property: ${key}`);
return target[key];
}
});
// Global variables
let temp1, temp2;
// effectFn1 is nested within effectFn2
effect(function effectFn1() {
console.log('effectFn1 executed');
effect(function effectFn2() {
console.log('effectFn2 executed');
// Access obj.bar property within effectFn2
temp2 = obj.bar;
});
// Access obj.foo property within effectFn1
temp1 = obj.foo;
});
effectFn1
is the outer effect
function, which depends on the value of obj.foo
. It contains an innerEffect
within it. The inner effect
function, effectFn2
, depends on the value of obj.bar
. When we modify obj.foo
, the outer effect
function should be triggered and output the value of obj.foo
. When we modify obj.bar
, the inner effect
function should be triggered and output the value of obj.bar
.
We use the global variable activeEffect
to store the effect functions registered through the effect
function. This means that at any given time, activeEffect
stores only one effect function.
// Use a global variable to store the currently active effect function
let activeEffect;
function effect(fn) {
// Define the effect function
const effectFn = () => {
// Call the cleanup function; specific implementation needs to be provided based on requirements
cleanup(effectFn);
// Assign the effect function to activeEffect
activeEffect = effectFn;
// Execute the effect function
fn();
// Store the dependencies of the current effect function in effectFn.deps (Needs to be implemented based on the actual logic)
effectFn.deps = []; // Set dependencies collection based on the actual logic
};
// Execute the effect function
effectFn();
}
However, by only using a single variable for storage without employing a stack structure, when nested effects occur, the execution of the inner effect function will overwrite the value of activeEffect
. It will never be restored to its original value. If there is a reactive data performing dependency collection, even if it is read within the outer effect function, the collected effect functions will all be from the inner effect function. In other words, when I read obj.foo
, activeEffect
still refers to the value of innerEffect
, and only the innerEffect
is triggered.
To solve this issue, we need an effect function stack called effectStack
. When an effect function is executed, the current effect function is pushed onto the stack. After the execution of the effect function is completed, it is popped from the stack. The activeEffect
is always set to the top effect function on the stack. This ensures that a reactive data will only collect the effect function that directly reads its value, preventing mutual interference:
// Use a global variable to store the currently active effect function
let activeEffect;
// Effect stack
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn); // Call the cleanup function; specific implementation needs to be provided based on requirements
activeEffect = effectFn;
// Push the current effect function onto the stack
effectStack.push(effectFn);
fn();
// After the execution of the current effect function is completed, pop it from the stack, and restore activeEffect to its previous value
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
// Initialize the dependencies collection of the effect function
effectFn.deps = [];
// Execute the effect function
effectFn();
}
Note: This article is a translated version of the original post. For the most accurate and up-to-date information, please refer to the original source.