preface
Computed in Vue is a property that is often used in daily development, and it is also a topic that is often asked in interviews. You can find a question like this in almost any interview question collection related to Vue: What is the difference between methods and computed? Without hesitation, you might say, “Methods don’t get cached, computed does.” Sure, this cache is a major feature, but what does this cache mean? How is caching implemented? In which case will it not be cached? When will the cache be reevaluated? What are the benefits of caching? In addition to caching, we can also ask: How do WE use setters in evaluating properties? Can calculated attributes depend on other calculated attributes? What is the internal mechanism? Many of you may not be familiar with these questions, but this article will give you an in-depth understanding of this computational property. Don’t be afraid to ask any interviewer.
The Vue source version used in this article is 2.6.11
DEMO
Let’s start with a simple example that will be analyzed in this article:
<div id="app">
<div @click="add">DoubleCount: {{doubleCount}}</div>
</div>
<script>
new Vue({
el: '#app'.name: 'root'.data() {
return {
count: 1}},computed: {
doubleCount() {
return this.count * 2}},methods: {
add() {
this.count += 1}}})</script>
Copy the code
Here we use a doubleCount calculation property, which is twice the value of count. Each click increases the value of count by one, and doubleCount changes accordingly.
The principle of analysis
First of all, you have to understand the principle of Vue’s responsive system. If you don’t understand it, you can go to the Internet to search for articles in this area.
The Vue source code posted in this article is not the original source code, in order to facilitate the analysis, the original source code has been simplified, in addition to unimportant logic and boundary case processing.
Look directly at the source code
Initialization process
The initState function is executed when the component is initialized:
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
After data is initialized, initComputed is used to initialize the properties of props, data, and methods. This is why we can access the properties of props, data, and methods directly in the computed properties, because the initialization occurs after these three properties. Here’s the logic for initComputed:
// VM is the component instance, and computed is the object we defined in options.
function initComputed(vm: Component, computed: Object) {
// Create a watchers object, which is empty
const watchers = (vm._computedWatchers = Object.create(null))
for (const key in computed) {
// Get the definition of the calculated property. For our example, userDef is the doubleCount function
const userDef = computed[key]
// Since doubleCount is a function, the getter here is still doubleCount
const getter = typeof userDef === 'function' ? userDef : userDef.get
// Create a Watcher and save it to Watchers
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
/ / to speak
defineComputed(vm, key, userDef)
}
}
Copy the code
This function is computed by traversing defined, creating a Watcher for each calculated attribute and saving it in Watchers, which is on the _computedWatchers attribute of the VM, A computedWatcherOptions is passed in to create a watcher, which is an object with only lazy attributes:
const computedWatcherOptions = { lazy: true }
Copy the code
Here’s a quick look at Watcher:
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.vm = vm
// Options are computedWatcherOptions, so lazy is true
this.lazy = !! options.lazy// To control the cache, more on that later
this.dirty = this.lazy // true
this.deps = [] // Collected Dep
// The evaluation method, for our example, is the doubleCount function
this.getter = expOrFn
// Initialize value. Lazy is true, so nothing is executed
this.value = this.lazy ? undefined : this.get()
}
}
Copy the code
The key points here are the lazy attribute and the dirty attribute. Lazy evaluates lazily and does not evaluate when the value is initialized because lazy is true. We’ll talk about dirty later, but then we’ll look at computed initialization.
DefineComputed (VM, key, userDef) is also executed after watcher is created:
export function defineComputed(
target: any, // vm
key: string, // Computed key: 'doubleCount'
userDef: Object | Function // Calculate the value of the attribute, the doubleCount function
) {
// Set the getter and setter using defineProperty
Object.defineProperty(target, key, {
enumerable: true.configurable: true.get: function computedGetter() {
// Get the Watcher created in initComputed
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/ /!!!!! Evaluate is executed only when dirty is true
if (watcher.dirty) {
// evaluate evaluates watcher and sets dirty to false
watcher.evaluate()
}
/ / to speak
if (Dep.target) {
watcher.depend()
}
// Return watcher
return watcher.value
}
}
})
}
Copy the code
DefineComputed sets up the proxy primarily through defineProperty, and this get function is executed when the calculated property is accessed through the instance.
Implementation of caching
Imagine a scene being rendered for the first time, count is 1, and accessing the doubleCount property in the template executes the get function defined in defineComputed, which first gets the watcher defined in initComputed, Evaluate (); evaluate(); evaluate()
class Watcher {
constructor() {
// ...
}
evaluate() {
this.value = this.get() // Get evaluates watcher, more on that later
this.dirty = false // Reset dirty to false}}Copy the code
Here the get function executes, which executes watcher’s getter, or in our case, doubleCount: Return this.count * 2, assigns the result to value, sets dirty to false, and then watcher has a value. Return watcher. Value returns the value of watcher and renders doubleCount in the template: In mounted console.log(this.doublecount), we will return to defineComputed get. So instead of executing watcher.evaluate(), doubleCount will simply return watcher.value, which is 2, and cache it.
If we change count from 1 to 2, then the next time we visit doubleCount, we should get 4, so when was this cache updated, and how was it updated? Don’t worry. Let’s move on.
The cache update
First, let’s review the flow of Vue’s responsive system. Vue’s responsive system is mainly implemented through Watcher, Dep and Object.defineProperty. When initializing data, Set the getter and setter for the property with Object.defineProperty to make the property responsive, and then create a Watcher when performing some operation (render operation, calculate property, customize Watcher, etc.). The watcher points a global variable, dep. target, to itself before performing the evaluation. Then, if a reactive property is accessed during the evaluation, the current dep. target (watcher) is added to the property’s Dep. Then, the next time the reactive property is updated, The collected watcher is retrieved from the DEP, and the update operation is performed by executing watcher.update.
Summary of the relatively brief, if you do not understand the words, it is suggested to go online to search the article in this respect
Watcher.evaluate () = this.value = this.get(); watcher.evaluate(); Let’s take a look at this.get
class Watcher {
constructor() {
// ...
}
get() {
// targetStack saves the current Watcher stack
// Other watcher may be created during watcher evaluation
targetStack.push(this)
// Point dep. target to itself
Dep.target = this
let value
const vm = this.vm
// Execute the getter function, which for our example is the doubleCount function
value = this.getter.call(vm, vm)
// The current watcher is off the stack
targetStack.pop()
// Revert to the previous watcher
Dep.target = targetStack[targetStack.length - 1]
return value
}
}
Copy the code
Dep.target is set and the getter is executed. DoubleCount accesses the count property, so it is executed in the getter of count:
function defineReactive(obk, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter() {
const value = val
// Dep.target is the watcher that calculates the property
if (Dep.target) {
// Execute depend to collect dependencies
dep.depend()
}
return value
},
set: function reactiveSetter(newVal) {
/ /... Speak later}})}Copy the code
This get basically does dep.depend() collecting dependencies:
class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this)}}}Copy the code
Here we execute Watcher’s addDep, passing itself in as a parameter:
class Watcher {
addDep(dep) {
dep.addSub(this)
this.deps.push(dep)
}
}
Copy the code
To add deP to Watcher’s own DEps, we call dep.addsub (this) with the argument itself, which returns to deP:
class Dep {
addSub(sub) {
this.subs.push(sub)
}
}
Copy the code
Subs = dep = watcher; subs = dep = watcher; subs = dep = watcher Deps will be the dep of [count], and both have references to each other. We can draw the conclusion that calling a deP’s Depend method adds dep. target to its subs (which we’ll use later). This is what we do when we initialize the value, and when we set count to 2, we go to setter logic for count:
function defineReactive(obk, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter() {
/ /...
},
set: function reactiveSetter(newVal) {
val = newVal
const subs = dep.subs.slice()
// Iterate over subs, execute update
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
})
}
Copy the code
Here we retrieve the previously saved watcher, iterate through it and execute watcher.update:
class Watcher {
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}}Copy the code
This is the key logic, because lazy is true to evaluate the attribute, so this.dirty = true is executed, and that’s it. If the logic ends here, where does the calculated property get reevaluated? Where does the view get rerendered? If this logic is followed, the calculated properties are not updated at all, and the view is not re-rendered, what is the problem?
How views are updated
One thing we’ve been missing is render Watcher, and when you render watcher, you do render Watcher first, and then you do render function in render Watcher, and then you call doubleCount in the render function, Watcher.get () : evaluate() : evaluate() : evaluate() : evaluate();
class Watcher {
constructor() {
// ...
}
get() {
// Access to doubleCount takes place in render Watcher
// So before executing the following line of code, inside the targetStack is: [render watcher]
targetStack.push(this) // After executing this code, the targetStack is: [render watcher, calculate Watcher]
Dep.target = this
let value
const vm = this.vm
// Collect dependencies as before
// dep.subs of count will be [calculate attribute watcher], calculate attribute watcher deps will be [count dep]
value = this.getter.call(vm, vm)
// The current watcher is off the stack
targetStack.pop() // After executing this code, the targetStack will be: [render watcher]
// Revert to the previous watcher
Dep.target = targetStack[targetStack.length - 1] // Dep.target is: render watcher
return value
}
}
Copy the code
Return to the getter for defineComputed after executing this get:
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// Evaluate watcher
watcher.evaluate()
}
// Dep.target is the render watcher, so there is a value here
if (Dep.target) {
// Perform watcher's collection dependency operation
watcher.depend()
}
return watcher.value
}
}
Copy the code
Since dep.target has a value, it executes watcher.depend().
class Watcher {
constructor() {
// ...
}
depend() {
// 上文已经分析过,计算watcher的deps是:[count的dep]
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
Copy the code
Here we iterate through dePS and execute deP’s Depend method, remember that method and that conclusion? Dep. Target = render watcher; deP. Target = render watcher; deP. After executing the Depend, the dep.subs of count is [calculate property watcher, render watcher].
So you might see by now, when you update a reactive property, in the setter for count, you iterate through the SUBs of the DEP and perform the update method, and in that subs you don’t just have the watcher that calculates the property, you also have the render watcher, Let’s look at the update method again:
class Watcher {
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}}Copy the code
Update to evaluate attributes, set dirty to true, then render watcher update, render Watcher lazy and sync to false, so queueWatcher(this), The queueWatcher method is a queueWatcher method that you don’t need to worry about. It actually ends up executing the render function in Watcher, which in turn calls doubleCount:
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// At this point dirty is already true, meaning it needs to be updated
if (watcher.dirty) {
// To evaluate watcher, execute doubleCount
// After execution, watcher.value will change from 2 to 4
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value / / return 4}}Copy the code
Since we set dirty to true in the update phase, we execute watcher.evaluate(), and doubleCount is updated and renders 4 on the page. If we change count again, The logic above is repeated.
If you don’t use doubleCount in the template, just listen for the calculated properties through Watch, which is similar logic, but change the render watcher to the user watcher. You can also break the process by yourself.
AAA calculates watcher,AA calculates watcher,A calculates watcher, render watcher]
conclusion
The following two points can be concluded from this paper:
- The lazy value of the calculated property watcher is true when modifying the reactive property
watcher.update
, instead of evaluating watcher, willwatcher.dirty
Set it to true, and watcher will be evaluated only when dirty is found to be true the next time the calculated property is accessed. - If the dependency of the evaluated property does not change, it will not be reevaluated no matter how many times we access it, and will be directly evaluated from
watcher.value
Return the value we need.
Many articles on Vue performance tuning refer to placing computation-intensive or frequently performed operations in calculation properties. It makes use of the characteristics of the compute attribute cache to reduce the meaningless calculation.
In addition to the content of this article, calculating the property also supports custom setters, and passing in other options, but it is relatively easy, you can read the source code analysis, if you thoroughly understand the content of this article, then whether in the interview or daily development, I believe you will be able to handle it easily.