Implementation Principle and Cleanup Mechanism of Nested Effects in Vue3's Reactivity System
1. Problem Description
In Vue3's reactivity system, an effect might execute in a nested manner (e.g., component rendering triggers another effect). If nested dependencies are not handled properly, it can lead to confusion in dependency collection (such as a parent effect depending on properties of a child effect). Vue3 addresses this issue through an effect stack and an activeEffect switching mechanism.
2. Core Concepts: Effect Stack and Active Effect
- Effect Stack (effectStack): Used to store
effectfunctions that are currently being executed, pushed/popped in execution order. - ActiveEffect: The currently active effect being processed; dependencies are associated with this variable during collection.
3. Execution Flow of Nested Effects
Assume the following code:
effect(() => {
console.log("Parent effect executed");
effect(() => {
console.log("Child effect executed");
});
});
Step 1: Initial Execution
- The outer effect is pushed onto the effect stack, and
activeEffectpoints to the parent effect. - The function body of the parent effect begins execution.
Step 2: Inner Effect Execution
- Upon encountering the inner effect, the execution of the parent effect is first paused. The inner effect is pushed onto the stack, and
activeEffectswitches to the child effect. - The child effect's function body is executed, and its dependencies are collected.
Step 3: Inner Execution Completion
- The child effect finishes execution, is popped from the stack, and
activeEffectis restored to the parent effect. - The remaining code of the parent effect continues to execute.
Key Point:
The stack structure ensures that activeEffect always points to the effect currently being executed, preventing confusion in dependency collection.
4. Dependency Cleanup Mechanism (Avoiding Stale Dependencies)
When an effect re-executes, old dependencies must be cleaned up before new ones are collected. Vue3 achieves this through a deps array for reverse cleanup.
Step 1: Dependency Recording
Each effect instance maintains a deps array, storing all associated dependency sets (i.e., Set objects corresponding to reactive properties).
Step 2: Cleaning Up Old Dependencies
Before an effect re-executes, it iterates through deps, removing the effect from each dependency set to avoid leaving behind stale dependencies.
Step 3: Re-collection
When the effect function executes, dependencies are re-added to the latest dependency sets.
5. Source-Level Example (Simplified Version)
let activeEffect;
const effectStack = [];
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
this.deps = []; // Stores dependency sets
}
run() {
// Clean up old dependencies
cleanupEffect(this);
// Push onto stack and switch active effect
effectStack.push(this);
activeEffect = this;
// Execute function (triggers dependency collection)
this.fn();
// Pop from stack and restore previous effect
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
function cleanupEffect(effect) {
for (const dep of effect.deps) {
dep.delete(effect); // Remove the effect from the dependency set
}
effect.deps.length = 0; // Clear the deps array
}
6. Practical Scenario: Nested Component Rendering
- Parent component rendering triggers the parent effect, while child component rendering triggers the child effect.
- The effect stack ensures that child component dependencies are correctly associated with the child effect, not the parent effect.
7. Summary
- Nested Effects: Managed via a stack structure to ensure accurate dependency collection.
- Dependency Cleanup: Achieved through reverse association via
deps, avoiding stale dependencies. - Performance Optimization: Prevents unnecessary dependency associations, enhancing the efficiency of the reactivity system.