Hi, everyone ~ in the last article Vue2 responsive principle analysis (1) : From the design I talked about Vue2 is how to abstract and design responsive, data is how to achieve responsive, including dependency collection and bidirectional dependent record design ideas and key code. In this article, we take a look at one of the most powerful reactive features in Kangkang Vue: calculated properties. I will mainly analyze the implementation of computational attributes and key code from the point of view of functional requirements, hoping to bring you something you can’t see in other articles. The following content please read the first article and then better ~
Computed attribute computed
It is mentioned in the Vue documentation that computed attributes are designed to solve the problem of complex and difficult to understand expressions in templates. There is, of course, a solution to this problem that is defined by methods, but there is a very powerful feature of evaluating attributes: caching. This means that if the data on which the calculated property depends has not changed, the calculated property will not be recalculated when accessed again, and the cached result will be returned directly, which is very useful for calculating complex scenarios.
How does the implementation of computed properties fit into the responsive design we talked about earlier? Here’s a picture:
This diagram shows that when you declare a compute property, Vue converts to the getter + Watcher structure on the right of the diagram to perform all the functions of the compute property. Don’t worry if it seems a bit confusing, here’s how the calculated properties are implemented and work.
Implementation details
First came to the SRC/core/instance/state. The js file, a initComputed function, this function is to initialize the calculated properties, let’s look at the key part of the code:
function initComputed (vm: Component, computed: Object) {
// Add the _computedWatchers watcher to the VM object
const watchers = vm._computedWatchers = Object.create(null)
// ...
for (const key in computed) {
Setters are supported for computed properties. For brevity we will focus only on computed properties declared as functions
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ...
// Don't worry about server-side rendering
if(! isSSR) {// Notice that a watcher is generated for each calculated property and the function for the calculated property is passed in as a getter
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if(! (keyin vm)) {
// This is the descriptor that defines the calculated properties on the VM object
defineComputed(vm, key, userDef)
}
// ...}}Copy the code
This is the main implementation of the computed properties. First of all, let’s focus a little bit on the process and not get bogged down in too much detail, which we’ll talk about later. The key flow is described in the diagram above: Vue generates a Watcher for each calculated property and declares an access descriptor on the VM object with the same name as the calculated property, which the two will use together later. The computedWatcherOptions passed in by the Watcher constructor has a lazy: true attribute, which we’ll see for what it does.
Caching starts with the use of computed attributes
Below are the details and highlights of the implementation of computed properties. We’ll start with the defineComputed function above, again removing some of the server-side rendering logic and just looking at the main implementation details of the code:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// Forget server-side rendering
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
// Notice that createComputedGetter is called to generate the getter for the descriptor
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
// ...
}
// ...
// Generate an access descriptor on the VM with the same name as the compute attribute
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
In defineComputed we see that the get of the final descriptor is generated by createComputedGetter, which is the key of the keys
Before continuing, let’s recall the use of computed attributes and the use of caching. We usually define computed properties and use them in the template. When the screen is first displayed, computed properties are computed, and unless the computed property’s dependency has changed (for example, the property of the dependent data object has been reassigned), subsequent refreshes will not cause the computed property to be recalculated, but will directly return the last cached value. From here you can see, to read computing the value of the attribute in the template, is actually called the vm on the get property descriptors.
With the scenario sorted out, we split the createComputedGetter into two parts, focusing on the first half of the cache-related code:
function createComputedGetter (key) {
return function computedGetter () {
// First get watcher on the VM here
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
// ...
return watcher.value
}
}
}
Copy the code
In the above code, watcher.dirty is used to calculate whether the current value of the property needs to be recalculated. If it does not need recalculation, it returns watcher.value directly. So here we recall that the calculated property initializes the Watcher with a lazy: true, and the watcher constructor has logic like this:
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
// ...
this.lazy = !! options.lazy// ...
this.dirty = this.lazy // It is true when initialized
/ /...
// Initializing watcher does not evaluate if the property is evaluated
this.value = this.lazy
? undefined
: this.get()
}
}
Copy the code
If you evaluate a property, watcher will not be evaluated when initialized, just marked as dirty — the cache is invalid. The first time watcher.dirty is called, watcher.evaluate() will be called:
evaluate () {
this.value = this.get()
this.dirty = false
}
Copy the code
Get () is performed in evaluate, and the mark cache is valid. Recall that watcher.update() is executed when a set occurs on the dependent DEP:
update () {
if (this.lazy) {
// If it is a calculated attribute, it will be marked as dirty
this.dirty = true
}
// ...
}
Copy the code
So when the dependency changes, the cache is invalidated and the calculated properties are recalculated
So that’s the design and implementation details of the computed property cache. I’m trying to just pick the key code and make the key things clear. The important thing to note here is that Vue abstracts the scenario of evaluating attributes into a lazy watcher, which only computes values when needed and has caching capabilities! So abstract ability is worth learning ~
Depend on the transfer
Let’s go back to the second half of the createComputedGetter:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// Check whether the cache should be updated
// ...
// The dependency is passed
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
Copy the code
We’ve covered caching in the first half, but now we notice that there’s a pass-by-pass logic. What does that mean?
Let’s use the scenario as an example. For example, you can refer to another computed property in the declaration of the computed property! Because the calculated property is not evaluated when the watcher is initialized, which is lazy Watcher, this is fine. For example:
const vm = new Vue({
el: '#demo'.data: {
a: 1.b: 2
},
computed: {
c: function () {
return a + b
},
d: function () {
return c * 2}}})Copy the code
Here’s a simple example to illustrate why dependency passing is needed: evaluating property C depends on A and B on data, and evaluating property D depends on C. So the problem is that c gets dirty when a and B change, and of course D also needs dirty, otherwise D would have a cache and not be reevaluated. So how does D get notified?
The key code is watcher.depend() above. First, d will call its own watcher. Get (), which sets d’s watcher to dep.target. Then c’s watcher. Get () will set C’s watcher to dep.target. Note that pushTarget is called to set dep. target. This function calls the targetStack array to push the currently recorded dep. target into the array:
Dep.target = null
const targetStack = []
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}// It was stated that the targetStack array would be parsed in a later article, so the circle is back
Copy the code
After this, c’s Watcher first establishes a dependency on THE DEP of A and B, and then evaluates watcher.evaluate(). PopTarget () = popTarget(); popTarget() = popTarget(); popTarget() = popTarget();
If c is evaluated and dep.target exists, then watcher.depend() will be called to pass the dependency.
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
Copy the code
D = deps; d = deps; d = dePs; So d gets dirty when A and B change.
From the description of the above scenario, you can see why we need to rely on passing
At the end
The above is the details of Vue calculation attributes and my interpretation, if there is not clear please combine with the first article to see.
So that’s pretty much the response for Vue2. I’ll write another article about listening properties later, and then go back to the design to consolidate it in general. If you are wrong or have other opinions, please leave a comment
Welcome star and follow my JS blog: Whisper By JavaScript