Two months ago I translated a post in Denver about how Computed works in Vue, but the translation was mediocre so I won’t post the address. One of my predecessors, who I admire very much, commented on this article with the title, “I feel that the original article did not address the essence of the Computed implementation – lazy Watcher.” Over the weekend, I just had a look at Vue source code, specifically looked at Computed, and shared my results with you.
Tips: If you haven’t seen the Vue source code before or don’t know much about Vue data binding, I recommend reading my previous articleEasy to understand Vue data binding source interpretation, or any other forum or blog post (there are plenty of these online). Because to understand this article, is the need for this knowledge point.
oneinitComputed
First, assume that a set of computed is passed in:
Data_one: data_two: data_one: data_two
computed:{
isComputed:function(){
return this.data_one + 1;
},
isMethods:function(){
return this.data_two + this.data_one; }}Copy the code
We know that new Vue() initializes data,props, methods, and computed in Vue:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // Initialize props
if (opts.methods) initMethods(vm, opts.methods) // Initialize methods
if (opts.data) {
initData(vm) // Initialize data
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed) // Initialize computed
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch)// Initialize initWatch}}Copy the code
I covered the initData() function in detail in the data binding article, but this time I’ll focus on initComputed().
const computedWatcherOptions = { lazy: true } // An object to pass into the Watcher instance
function initComputed (vm: Component, computed: Object) {
// Declare one watchers and mount it to the Vue instance
const watchers = vm._computedWatchers = Object.create(null)
// Whether server render
const isSSR = isServerRendering()
// Iterate over the incoming computed
for (const key in computed) {
//userDef is each method on a computed object
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if(process.env.NODE_ENV ! = ='production' && getter == null) {
warn(
`Getter is missing for computed property "${key}". `,
vm
)
}
// Create a Watcher instance if it is not rendered by the server
if(! isSSR) {// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if(! (keyin vm)) {
// If the key in computed is not in the VM, mount it through defineComputed
defineComputed(vm, key, userDef)
} else if(process.env.NODE_ENV ! = ='production') {
// For computed, the key is the same name
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
Copy the code
Before initComputed, we saw that a computedWatcherOptions object was declared, which is the key to implementing a “lazy Watcher.”
Next, look at initComputed, which prefixes an empty object called Watchers and mounts the empty object on the VM. It then iterates over the evaluated properties and assigns the method for each property to userDef, a getter if userDef is a function, and then determines if it is a server rendering, and creates a Watcher instance if it is not. The Watcher instance I analyzed in the last article will not be analyzed line by line, but it is important to note that in the new instance we pass in the fourth parameter, computedWatcherOptions, and the logic in the Watcher will change:
/ / this code in the Watcher, the file path for the vue/SRC/core/observer/Watcher. Js
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync }else {
this.deep = this.user = this.lazy = this.sync = false
}
Copy the code
The options here are computedWatcherOptions. When we use initData logic, the options do not exist, so this.lazy = false. This. Lazy = true. Dirty = this.lazy, and the dirty value is true.
this.value = this.lazy
? undefined
: this.get()
Copy the code
From this code we can see that when lazy is false, undefined is returned instead of this.get(). That is, the two methods in computed are not executed :(see the computed example I wrote at the beginning)
function(){
return this.data_one + 1;
}
function(){
return this.data_two + this.data_one;
}
Copy the code
This means that the computed value has not been updated. And that logic is over for the moment.
2. DefineProperty
Let’s go back to the initComputed function:
if(! (keyin vm)) {
// If the key in computed is not in the VM, mount it through defineComputed
defineComputed(vm, key, userDef)
} Copy the code
As you can see, the defineComputed function is executed when the key value is not mounted to the VM:
// An object used to assemble defineProperty
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop,
set: noop
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
// If it is a server render, note the variable name => shouldCache
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
/ / if userDef is function, give sharedPropertyDefinition. Get, that is, the current key getter
/ / assign createComputedGetter (key)
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
// Otherwise, use userDef.get and userDef.set to assignsharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if(process.env.NODE_ENV ! = ='production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`.this)}}// Finally, we mount the key to the VM
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
DefineComputed determines whether the attribute is rendered on the server. If it is not, the attribute should be cached, which means shouldCache is true. Next, determine if userDef is a function, which is our regular computed use, and set the getter to the return value of createComputedGetter(key). Get and userDef.set are used to assign values to the getters and setters. I won’t go into the else part, but those of you who don’t have custom computed can see the documentation for the setters of computed properties. Finally, mount the key for computed to the VM, and the getter is called when you access the computed property.
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}Copy the code
Finally, let’s look at the createComputedGetter function, which returns a function computedGetter, and determine if watcher. Dirty exists, if watcher exists. Based on the previous analysis, The first time a new Watcher instance is created, call watcher.evaluate() if this.dirty is true:
function evaluate () {
this.value = this.get()
this.dirty = false
}Copy the code
This.get () is actually the method that executes the evaluated property. Then set this.dirty to false. In addition, dep.target is assigned when we execute this.get(), so we also execute watcher.depend() to add the watcher that evaluates the attribute to the dependency. Finally, we return watcher.value. Finally, we get the value of the computed property and complete the initialization of computed.
Calculate the cache of properties — lazy Watcher
However, at this point we haven’t addressed the main point of this article, which is “lazy watcher.” Remember that the Vue official document describes computed like this:
We can define the same function as a method rather than a computed property. The end result is exactly the same either way. However, here’s the difference
Computed properties are cached based on their dependencies. A computed property is reevaluated only if its associated dependencies change. And that means as long as
message
It hasn’t changed. Multiple visits
reversedMessage
The evaluated property immediately returns the previous evaluated result without having to execute the function again.
Reviewing the previous code, we found that as long as the value of the data property in the evaluated property is not updated, watch.lazy is always false after the first value is obtained, and watcher.evaluate() is never executed, so the evaluated property is never re-evaluated. Always use the value that was last obtained (known as the cache).
Once the value of the data property changes, we know that update() will be triggered and the page will be rerendered (this part is a bit of a jump, you must understand the principle of data binding), and then initComputed again. Then this.dirty = this.lazy = true, and the evaluated property is revalued.
OK, that’s all I have to say about computed, but this article leaves a hole in the createComputedGetter function:
const watcher = this._computedWatchers && this._computedWatchers[key]Copy the code
We can infer from the context that this._computedWatchers must hold the watcher instance created on initComputed, but when do we put the watcher instance into this._computedWatchers? I have not found, if you know the friend please leave a message to share, we discuss together, thank you very much!