If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

Nested Effects and Effect Stack

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.


vue-watch-computed The Role and Implementation of Vue.js Reactive System

Comments