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.
```
Comments