We can see from the official documentation that we can use computed property computed when we need to do something complex in a template that relies on some data to get a result. When data changes, the value of the calculated property changes, and the view is updated. The official documentation also mentions that the result of a calculated property is recalculated only when the reactive data on which the calculated property depends is changed, so the calculated property also has the property of caching values.
So this article takes a look at how Vue implements these features of computed from the source code.
Initialize the
Let’s start with the initialization part of computed.
Initialization of computed occurs when the initState method is executed at Vue instantiation (source address).
// vue/src/core/instance/init.js.export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? :Object) {... initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm,'beforeCreate')
initInjections(vm)
initState(vm) / / initialize the state, including data/props/computed the methods/watch
initProvide(vm)
callHook(vm, 'created')... }}...Copy the code
Now look at what initState does:
// vue/src/core/instance/state.js.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) Initialize computed, and pass in the VM instance and a user-defined computed object
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } } ...Copy the code
Can see from the source, initState performed some state (data/props/the methods/computed/watch) related initialization, and then we find initComputed (source location with initState), Now look at what has been done with computed initialization.
// vue/src/core/instance/state.js.const computedWatcherOptions = { lazy: true } // computedWatcher configuration object
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
The initComputed function takes two parameters, one for the instance itself and the second for a user-defined computed object. The function first creates a Watchers empty object, then iterates through the computed object defined by the user, and evaluates each computed object defined by the user. If the value is a function, the function is stored directly as a getter. If the value is an object, The get property inside the object is stored as a getter. Then create a Watcher instance for each computed object, pass the previously saved getters and VM instances, and the computedWatcherOptions for computing attributes, as parameters to Watcher. Finally, store the Watcher instance in the Watchers object. What Watcher does will be analyzed later, just need to know this step. Finally, determine whether computed already exists in the VM instance, and if so, determine who conflicts with data/props, and then give a hint for the conflict. If not, execute defineComputed.
// vue/src/core/instance/state.js.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 defineComputed function defines setters and getters for computed properties and binds computed properties to the VM instance. Setting setters in defineComputed is just assignment, look at createComputedGetter, the setting function for getters.
// vue/src/core/instance/state.js.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
CreateComputedGetter is primarily a getter function that returns computed data. Now that the initialization of computed is over, how do three of its features work? Remember the Watchers object? The secret lies in the Watcher instance stored in this object.
computed watcher
As you can see from the getter function for computed, when computed is accessed, you are actually executing some methods of the Watcher instance object stored in initCompute. So what’s the secret behind Watcher?
Take a look at the Watcher methods and properties used when computed is accessed from the source code.
// vue/src/core/observer/watcher.js
export default class Watcher {
constructor (
vm: Component, / / vm instances
expOrFn: string | Function.// For computed data, this is the getter function for computed properties passed in 'initComputed'
cb: Function.// NoOP is passed in 'initComputed'
options?: ?Object.// 'initComputed' computedWatcherOptions object passed in, {lazy: true}isRenderWatcher? : boolean// This parameter is not passed in 'initComputed'
) {...// options
if (options) {
...
this.lazy = !! options.lazy ... }else{... }...this.dirty = this.lazy // for lazy watchers. }.../** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
evaluate () {
this.value = this.get()
this.dirty = false
}
/** * Depend on all deps collected by this watcher. */
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
...
}
Copy the code
The dirty attribute defaults to true, so when the evaluate attribute is accessed for the first time, the evaluate method executes the get method and stores the value of the get method into the value attribute. Finally change the value of dirty to false. Well, this brings us to the first feature of computed properties – cache values. If there is no other place to change the value of dirty, does that mean that when an evaluated property is accessed once, its watcher. Dirty is always false? So the watcher.value returned at the end of the property getter function is the saved result. So a caching feature is implemented.
Let’s first look at how the value of the computed property is computed. Get method!
// vue/src/core/observer/watcher.js./** * Evaluate the getter, and re-collect dependencies. */
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
...
Copy the code
When the get method executes, it pushes the current Watcher onto the active Watcher stack and then executes the getter method passed in when the Watcher instance is created to get the result of the evaluated property. So when the getter is executed, it accesses the data that the calculated property depends on, triggers the dependency collection of data, and saves the calculated property’s watcher to its own DEP.subs. This is where the data-dependent properties are computed. When the getter is finished, pop the current calculated property watcher from the active watcher stack, and change the dep. target to the first watcher in the current stack. Finally returns the result of the computed property.
Now we go back to calculating the property’s getter. When the calculation is done, we call the watcher.depend() method. The source code (source location) is as follows:
// vue/src/core/observer/dep.js. depend () {if (Dep.target) {
Dep.target.addDep(this)}}...Copy the code
AddDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep: addDep
// vue/src/core/observer/watcher.js./** * Add a dependency to this directive. */
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)}}}...Copy the code
AddDep will store the deP object passed in, and if the watcher has not saved the DEP, it will save the Watcher to the DEP. This ensures that data can always collect all observed objects. Specific data collection dependencies will be covered in subsequent articles. Let’s abstractly know the dependency collection of data used here. Since data collected all observers, when data is updated, all watcher will be notified to perform the update method (source location) :
// vue/src/core/observer/watcher.js./** * Subscriber interface. * Will be called when a dependency changes. */
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}...Copy the code
The update method determines whether the current Watcher is lazy or lazy. So for computedWatcher, you define that computedWatcher is lazy by passing in the computedWatcherOptions object when you create the watcher. So when data is modified and computed Watcher is updated, all it does is change the dirty property of computed Watcher to true, telling the computed property that you are “dirty” and need to be recalculated the next time you are accessed. After data is modified to notify computed Watcher of updates, it continues to notify other Watcher, such as the Render Watcher that has accessed computed properties, and then Render Watcher performs an update to access computed again, This computed object is already marked as dirty, so this time it recalculates to the new value.
When the data on which the calculated attributes depend is updated, the watcher will change the dirty of the calculated attributes to true. When the calculated attributes are accessed again, they will be reevaluated. Otherwise, The calculated property returns the previously saved value directly. In addition, computed Watcher does not evaluate directly during update, but only when the computed property is accessed again.
Data dependency collection, Watcher, and DEP will be updated gradually in the future, so stay tuned and come back soon!