Many times, it is not clear when to use Vue’s computed properties and when to use Watch to listen on properties. Now let’s take a look at the similarities and differences between the two from a source point of view.
computed
The initialization of computed properties takes place in the initState() function of the Vue instance initialization phase, where there is an initComputed function. The definition of the function in the SRC/core/instance/state in js:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
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
)
}
if(! isSSR) {// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if(! (keyin vm)) {
defineComputed(vm, key, userDef)
} else if(process.env.NODE_ENV ! = ='production') {
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
You start by creating an empty object, and then iterate over each key in the computed property, creating a Watcher for each key. This Watcher differs from regular Watcher in that it is lazy Watcher. We’ll expand on the difference between lazy Watcher and regular Watcher later. Then call defineComputed(VM, key, userDef) to determine if the key is not an attribute in the instance VM, otherwise raise the appropriate warning.
Next, focus on the implementation of defineComputed(VM, key, userDef) :
export function defineComputed (target: any, key: string, userDef: Object | Function) {
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.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)}}Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
The logic here is simple: Add getters and setters to the key value of the calculated property using Object.defineProperty. The final getter corresponds to the return value of the createdComputedGetter(key). Let’s look at its definition:
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
CreatedComputedGetter (key) returns a function computedGetter, which is the getter for the computed property.
At this point, the entire initialization process of the calculated properties is complete. We know that the calculated Watcher is a lazy Watcher. What’s the difference between a lazy Watcher and a regular Watcher? To analyze the implementation of lazy Watcher, use an example:
var vm = new Vue({
data: {
firstName: 'Foo'.lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Copy the code
The constructor logic is slightly different when initializing the entire lazy Watcher instance:
constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {/ /...
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
You can see that lazy Watcher does not immediately evaluate, but instead returns undefined.
Then when our render function calls this.fullname, it starts calculating the property’s getter:
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
These few lines of code are the core logic. The watcher. dirty attribute is true. Do watcher.evaluate () :
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
evaluate () {
this.value = this.get()
this.dirty = false
}
Copy the code
Here, the get function for the corresponding attribute is executed by calling this.get(). In our case, execute:
function () {
return this.firstName + ' ' + this.lastName
}
Copy the code
At this point, the corresponding variables firstName and lastName are fetched, triggering the corresponding reactive procedure. Once you have the latest value, set the this.dirty property to false.
The more critical code is here:
if (Dep.target) {
watcher.depend()
}
Copy the code
The Vue instance has a Watcher that invokes calculated properties. Among the calculated properties is lazy Watcher, which invokes reactive properties. Every Watcher’s get() method has pushTarget(this) and popTarget().
In the above code, the dep. target is the instance Watcher of Vue, and the Watcher variable is the lazy Watcher that evaluates the attribute. By executing the code watcher.depend(), Deps that associate lazy Watcher with evaluated attributes are all associated with dep.target.
In our case, we associate this.firstName and this.lastName with the instance Watcher. In this way:
- when
this.firstName
,this.lastName
When changes occur, the instanceWatcher
You will be notified of the update, and the calculated property will also trigger the GET function to update. - when
this.firstName
,this.lastName
When there is no change, the instanceWatcher
The call computes the property becauselazy Watcher
The correspondingdirty
Properties forfalse
, then it will be returned directly to the cachevalue
Value.
The lazy Watcher in the calculated attribute does the following:
- Get functions (methods) that save computed attributes
- Save calculation results
- Control whether cached calculations are valid (via this.dirty)
Watch
The initialization of listening properties, similar to that of evaluating properties, takes place in the initState() function of the Vue instance initialization phase, which has an initWatch function. The definition of the function in the SRC/core/instance/state in js:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
Copy the code
If the handler is an array, iterate through the array and call createWatcher:
function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options? : Object) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Copy the code
$watch(keyOrFn, handler, options); $watch(keyOrFn, options); It is defined when stateMixin is executed:
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
watcher.teardown()
}
}
Copy the code
That is, the listening property watch will eventually call the $watch method, which will first determine if cb is an object and call createWatcher. This is because the $watch method can be called directly by the user, passing an object as well as a function.
Finally, const watcher = new watcher (VM, expOrFn, CB, options) instantiates a watcher. One thing to note here is that this is a user Watcher because options.user = true. By instantiating Watcher, once our watch’s data changes, it will eventually execute Watcher’s run method, executing the callback function cb.
$watch = vm.$watch = vm.$watch = vm.$watch = vm.
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
Copy the code
There is a new article about error handling within Vue, which can be found here.
conclusion
In terms of application scenarios, computed attributes are suitable for template rendering, where a value is computed depending on other responsive objects or even computed attributes. The listening attribute is suitable for observing changes in a value to complete a complex piece of business logic.
Vue source code interpretation
(1) : Vue constructor and initialization process
(II) : Data response and implementation
(3) : array responsive processing
(iv) Vue’s asynchronous update queue
(5) The introduction of virtual DOM
(VI) Data update algorithm — Patch algorithm
(7) : realization of componentization mechanism
(8) : computing properties and listening properties
The Optimize stage of the compilation process
Vue
Vue’s error handling mechanism
Parse the working process of Vue in handwritten code
Vue Router handwriting implementation