preface
When people think of computed data in Vue, the first thing they think is that computed properties are cached, so how exactly is it cached? There are a lot of people who are still confused about what the cache is and when it expires.
This article, based on Vue 2.6.11, takes a closer look at what caching actually looks like.
Pay attention to
This article assumes that you already have a basic understanding of the Vue responsive principle. If you are not familiar with Watcher, Dep, and the concept of rendering Watcher, you can find some articles or tutorials on the basic responsive principle. If you want to see the simplified implementation, you can also see the article I wrote:
Learn Vue source code for Data, computed, and Watch by implementing a minimalist responsive system
Note that I have also written about the principle of computed tomography in this article, but computed in this article is based on Vue version 2.5, and the change from the current version 2.6 is very big, so it can only be used for reference.
The sample
In keeping with my convention, I’m going to use a very simple example.
<div id="app">
<span @click="change">{{sum}}</span>
</div>
<script src=". / vue2.6. Js. ""></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: 1,
}
},
methods: {
change() {
this.count = 2
},
},
computed: {
sum() {
return this.count + 1}},})</script>
Copy the code
This example is very simple. The number 2 is displayed at the beginning of the page, and when you click the number, it becomes 3.
parsing
Review watcher’s process
To get down to business, when Vue first runs, it does some initialization for computed properties. First, let’s review watcher’s concepts, whose core concepts are GET evaluation and update.
-
When evaluating, it first assigns itself, watcher itself, to the dep. target global variable.
-
And then when you evaluate, you read the reactive property, and the DEP of the reactive property collects this Watcher as a dependency.
-
The next time a reactive property is updated, it retrieves the watcher it collected from the DEP and triggers watcher.update() to update it.
The key is what the GET does and what updates the update triggers.
In the basic reactive view update process, the above concept of GET evaluation refers to the Vue component re-render function, and update, in fact, re-call the component render function to update the view.
The neat thing about Vue is that this process also works for computed updates.
Initialize the computed
For a preview, Vue also wraps every computed property in options with Watcher, and its GET function performs a user-defined evaluation function, while update is a more complex process, which I’ll explain in more detail.
First, when the component is initialized, it enters a function that initializes computed
if (opts.computed) { initComputed(vm, opts.computed); }
Copy the code
Go to initComputed and take a look
var watchers = vm._computedWatchers = Object.create(null);
// Define each computed attribute in turn
for (const key in computed) {
const userDef = computed[key]
watchers[key] = new Watcher(
vm, / / instance
getter, // The user passes in the evaluation function sum
noop, // The callback function can be ignored for now
{ lazy: true } // Declare the lazy attribute to mark computed Watcher
)
// what happens when the user calls this.sum
defineComputed(vm, key, userDef)
}
Copy the code
We first define an empty object to hold all the calculated property-related Watcher, which we will call the calculated Watcher later.
The loop then generates a calculated watcher for each computed property.
Its form retains key properties and, after simplification, looks like this:
{
deps: [].dirty: true.getter: ƒ sum (),lazy: true.value: undefined
}
Copy the code
Its value is undefined, and its lazy value is true, which means that its value is lazy and not evaluated until it is actually read from the template.
This dirty property is actually the key to caching, so keep it in mind.
Let’s look at the crucial defineComputed, which determines what happens when the user reads the value of the this.sum calculated property, further simplifying and eliminating some logic that doesn’t affect the process.
Object.defineProperty(vm, 'sum', {
get() {
// Get computed Watcher from the component instance just described
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// ✨ note! This is only reevaluated if it's dirty
if (watcher.dirty) {
// Get is evaluated here
watcher.evaluate()
}
// ✨ is also a key point
if (Dep.target) {
watcher.depend()
}
// Returns the calculated value
return watcher.value
}
}
})
Copy the code
This function needs to be looked at carefully. It does several things, and we’ll show it in the process of initialization:
First, the concept dirty refers to dirty data, indicating that the data needs to be evaluated again by calling the sum function passed in by the user. Leaving aside the logic at update time, {{sum}} must be true when it is first read from the template, so initialization goes through an evaluation.
evaluate () {
// Call the get function to evaluate
this.value = this.get()
// Mark dirty as false
this.dirty = false
}
Copy the code
This function is actually quite clear, it evaluates first and sets dirty to false.
Going back to the logic of object.defineProperty,
The next time there is no special case where sum is false, we can return the value watcher.value.
update
Now that the initialization process is over, you have a general idea of dirty and cache (if not, take a second look).
Let’s take a look at the update process. In the example of this article, how the update of count triggers the change of sum on the page.
Let’s go back to the evalute function, which evaluates sum when it reads dirty data.
evaluate () {
// Call the get function to evaluate
this.value = this.get()
// Mark dirty as false
this.dirty = false
}
Copy the code
Dep.target changed to render Watcher
If {{sum}} is read from the template, the dep. target should be the render watcher.
The global dep. target state is stored in a targetStack, which allows you to advance and reverse dep. target, as shown in the following function.
The dep. target is render watcher and the targetStack is [Render Watcher].
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
popTarget()
}
return value
}
Copy the code
PushTarget = Dep. Target; pushTarget = Dep. Target;
After pushTarget(this),
Dep.target is changed to calculate watcher
The dep. target is calculated watcher, and the targetStack is [Render watcher, calculate Watcher].
Value = this.getter.call(vm, vm),
The getter function, as explained in the Watcher form in the previous chapter, is the sum function passed in by the user.
sum() {
return this.count + 1
}
Copy the code
Here, at execution time, this.count is read, notice that it’s a responsive property, so somehow they start to make connections…
This is going to trigger get hijacking of count, just to simplify things
// In the closure, the deP defined for the key count is retained
const dep = new Dep()
// The closure also preserves the val set last time
let val
Object.defineProperty(vm, 'count', {
get: function reactiveGetter () {
const value = val
// dep. target is used to calculate watcher
if (Dep.target) {
// Collect dependencies
dep.depend()
}
return value
},
})
Copy the code
So you can see that count is going to be counted and watcher is going to be counted
// dep.depend()
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}Copy the code
AddDep (this) is the addDep function of Watcher. This is the result of some internal de-weighting optimization.
// Watcher's addDep function
addDep (dep: Dep) {
// A series of de-duplicates are done here to simplify things
// the dep of count is stored in its own deps
this.deps.push(dep)
// Take watcher itself as a parameter
// return to the addSub function of dep
dep.addSub(this)}Copy the code
It’s back to deP.
class Dep {
subs = []
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
Copy the code
This preserves the dependency in the DEP to count watcher as count.
After going through such a collection process, some states at this point:
Watcher:
{
deps: [dep of count],dirty: false.// the value is false
value: 2.// 1 + 1 = 2Getter: ƒ sum (),lazy: true
}
Copy the code
The count of dep:
{
subs: [sum calculation watcher]}Copy the code
As you can see, the Watcher that calculates the attributes and the DEP of the reactive values that it depends on preserve each other.
At this point, the evaluation is finished, and we are back to calculating watcher’s getter function:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
// This is the end of the execution
popTarget()
}
return value
}
Copy the code
PopTarget is executed, and watcher is calculated out of the stack.
Dep.target changed to render Watcher
The dep. target is render watcher and the targetStack is [Render Watcher].
Then the function completes and returns a value of 2, while the get access to the sum property is still in progress.
Object.defineProperty(vm, 'sum', {
get() {
// At this point the function is executed
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
})
Copy the code
The dep.target of course has a value, which is the render watcher, so the logic of watcher.depend() is very important.
// watcher.depend
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
Copy the code
Remember how we calculated watcher’s form? It holds the DEP of count in its DEPS.
That is, dep.depend() on count is called again
class Dep {
subs = []
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}}Copy the code
The dep. target is already the render Watcher, so the count Dep will store the render Watcher in its subs.
The count of dep:
{
subs: [sum calculated watcher, render watcher]}Copy the code
So that brings us to the point of this problem, how do we trigger view updates when count is updated?
Back to count’s reactive hijacking logic:
// In the closure, the deP defined for the key count is retained
const dep = new Dep()
// The closure also preserves the val set last time
let val
Object.defineProperty(vm, 'count', {
set: function reactiveSetter (newVal) {
val = newVal
// Trigger notify of count's dep
dep.notify()
}
})
})
Copy the code
Ok, this triggers the notify function of the deP of count that we’ve just carefully prepared, and it feels like we’re getting closer to success.
class Dep {
subs = []
notify () {
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
The logic here is simple: call the update method of the watcher saved in subs
- call
Calculate the watcher
The update of - call
Render the watcher
The update of
Let’s break it down.
Calculate the update to watcher
update () {
if (this.lazy) {
this.dirty = true}}Copy the code
WTF, that’s all… That’s right, just set the dirty property of watcher to true and wait for the next read.
Render watcher update
Call vm._update(vm._render()) and re-render the vNode based on the render function.
Sum = sum; sum = sum; sum = sum; sum = sum;
Object.defineProperty(vm, 'sum', {
get() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// ✨ set dirty to true in the previous step, so it will be reevaluated
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
// Returns the calculated value
return watcher.value
}
}
})
Copy the code
Due to the reactive attribute update in the previous step, the dirty update of the calculated Watcher is triggered to true. So the sum function passed in by the user is called again to calculate the latest value, which is displayed on the page.
At this point, the entire process of calculating property updates is complete.
The cache takes effect
According to the summary above, dirty is reset to true only when the reactive value of the calculated attribute dependence is updated, so that the actual calculation will occur the next time it is read.
In this case, the optimization is obvious, assuming that the sum function is a user-defined and time-consuming operation.
<div id="app">
<span @click="change">{{sum}}</span>
<span @click="changeOther">{{other}}</span>
</div>
<script src=". / vue2.6. Js. ""></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: 1.other: 'Hello'}},methods: {
change() {
this.count = 2
},
changeOther() {
this.other = 'ssh'}},computed: {
// Very time consuming computing properties
sum() {
let i = 9999999999999999
while(i > 0) {
i--
}
return this.count + 1}},})</script>
Copy the code
In this example, the value of other has nothing to do with the calculated property. If the value of other triggers an update, the view will be rerendered and sum will be read. If the calculated property is not cached, a performance-costly and unnecessary calculation will occur every time.
So, sum is recalculated only if count changes, which is a neat optimization.
conclusion
The path for calculating property updates in version 2.6 looks like this:
- Reactive value
count
update - At the same time inform
computed watcher
和Render the watcher
update computed watcher
Set dirty to true- View rendering reads values for computed, due to dirty
computed watcher
Reevaluate.
With this article, you have a good understanding of what caching of computed attributes really is and under what circumstances it works.
For cached and uncached cases, the flow looks like this:
Don’t cache:
count
Change, notice firstCalculate the watcher
Update, setdirty = true
- Notice to
Render the watcher
Update when the view is re-renderedCalculate the watcher
Medium read valuedirty
If true, re-execute the function evaluation passed in by the user.
Cache:
other
Change, direct noticeRender the watcher
The update.- Go when the view is re-rendered
Calculate the watcher
Medium read valuedirty
If false, use the cached value directlywatcher.value
Does not perform the function evaluation passed in by the user.
Looking forward to
In fact, this method of calculating attribute cache through dirty flag bits is the same as Vue3 implementation principle. This may also indicate that, given the variety of needs and community feedback, Utah now considers this approach to be a relatively optimal solution for computed caching.
If you are interested in a computed implementation of Vue3, you can also read this article. The principle is very much the same. It’s just a slightly different way of collecting.
In-depth analysis: How does Vue3 implement powerful computed smartly
❤️ thank you
1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.
2. Follow the public account “front-end from advanced to hospital” to add my friends, I pull you into the “front-end advanced communication group”, we communicate and progress together.