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

vue-watch-computed

Introduction

Previously, we discussed the effect function, which is used to register side-effect functions. It allows specifying options parameters, such as the scheduler to control the timing and manner of side-effect function execution. We also explored the track function for dependency tracking and the trigger function to re-execute side-effect functions. Combining these concepts, we can implement a fundamental and distinctive feature of Vue.js – computed properties.

Computed Properties and the lazy Option

In Vue.js, the effect function is used to create reactive side-effect functions. By default, side-effect functions passed to effect are executed immediately. For example, in the code below, the side-effect function passed to the effect function is executed immediately:

effect(() => {
  console.log(obj.foo);
});

However, in certain cases, we want the side-effect function to execute only when needed, not immediately. A typical scenario is with computed properties. To achieve this delayed execution, we can add a lazy property to the options object and set it to true. When lazy is true, the side-effect function is not executed during initialization but only when necessary. The modified code looks like this:

effect(
  // This function will not execute immediately
  () => {
    console.log(obj.foo);
  },
  // options
  {
    lazy: true
  }
);

In the implementation, the side-effect function effectFn is returned as the result of the effect function. This means that when we call the effect function, we get the corresponding side-effect function and can manually execute it when needed. This mechanism gives us more control, allowing us to decide when to trigger the execution of the side-effect function rather than executing it immediately.

This design pattern is particularly suitable for specific scenarios like computed properties. In computed properties, we might want to trigger the side-effect function’s execution at a specific moment rather than immediately during initialization. By returning the side-effect function from the effect function, we can flexibly control when the side-effect function is executed to meet different requirements in various scenarios.

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };

  // Set options and dependencies for the side-effect function
  effectFn.options = options;
  effectFn.deps = [];

  // Execute the side-effect function only if it's not lazy
  if (!options.lazy) {
    effectFn();
  }

  // Return the side-effect function as the result
  return effectFn;
}

In this code, the effect function’s second parameter is an options object, where the lazy property is set to true. This means that the side-effect function passed to effect will be executed only when necessary, such as when accessing a computed property. The lazy property allows us to control the immediate execution of the side-effect function.

Now that we have achieved lazy computation through computed properties, how do we implement data caching?

function computed(getter) {
  // value is used to cache the last computed value
  let value;
  // dirty flag indicates whether a recalculation is needed; if true, it means "dirty" and needs computation
  let dirty = true;

  const effectFn = effect(getter, {
    lazy: true
  });

  const obj = {
    get value() {
      // Compute the value only if it's "dirty," and cache the computed value in the value variable
      if (dirty) {
        value = effectFn();
        // Set dirty to false, so the cached value can be used directly next time
        dirty = false;
      }
      return value;
    }
  };

  return obj;
}

With lazy computation resolved, the value is calculated only when it is truly needed, executing effectFn only when necessary. Additionally, a dirty flag is introduced to indicate whether the current computation needs to be recalculated. If dirty is true, the value is recalculated, and the dirty flag is set to false so that the cached value can be used directly next time.

Implementation Principle of watch

The concept of watch essentially involves observing a reactive data and executing the corresponding callback function when the data changes. For example:

watch(obj, () => {
  console.log('Data changed');
})
// Modifying the reactive data triggers the execution of the callback function
obj.foo++

Suppose obj is a reactive data, watched using the watch function with a provided callback function. When modifying the reactive data’s value, the callback function is triggered. In fact, the implementation of watch essentially utilizes the effect and the options.scheduler option, as shown in the following code:

effect(() => {
  console.log(obj.foo)
}, {
scheduler() {
// The scheduler function is executed when obj.foo's value changes
}
})

In a side-effect function accessing the reactive data obj.foo, based on the previous discussion, we know that this establishes a connection between the side-effect function and the reactive data. When the reactive data changes, the side-effect function is re-executed. However, there is an exception: if the side-effect function has a scheduler option, when the reactive data changes, the scheduler function is executed instead of directly triggering the side-effect function. From this perspective, the scheduler function acts as a callback function, and the implementation of watch utilizes this characteristic.

Below is the simplest implementation of the watch function:

// The watch function receives two parameters: source (reactive data) and cb (callback function)
function watch(source, cb) {
  effect(
    // Trigger a read operation to establish a connection
    () => source.foo,
    {
      scheduler: scheduler(),
      // Call the callback function cb when the data changes
      fn: () => {
        cb();
      },
    }
  );
}

In this code, we first define an original data object named data, which contains a property foo with an initial value of 1. Next, we create a proxy object obj using Proxy, intercepting operations on the data.

When obj.foo++ is executed, the set interceptor of Proxy is triggered. In the set interceptor, we first set the property value on the target object and then call the watch function, passing obj and a callback function. In the watch function, we use a hypothetical effect function (which might be provided by a framework) to listen for data changes.


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-expired-side-effects Nested Effects and Effect Stack

Comments