Study notes and keep adding to them
1. Example (website)
var vm = new Vue({
data: { a: 1 },
computed: {
// Calculates the getter for the property
/ / read only
aDouble: function () {
// 'this' points to the VM instance
return this.a * 2
},
// Read and set
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}
}
})
vm.aPlus / / = > 2
vm.aPlus = 3
vm.a / / = > 2
vm.aDouble / / = > 4
Copy the code
The result of the evaluated property is cached, except for recalculation of dependent responsive property changes. Note that if a dependency (such as a non-reactive property) is outside the instance scope, the calculated property is not updated.
What is a calculated attribute? Why is it cached? Let’s move on
2. Source code parsing (2.5.17beta version, more calculation, less rendering)
Here combined with a scene example, intuitive analysis of the implementation of the calculation of attributes
var vm = new Vue({
data: {
firstName: 'Foo'.lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Copy the code
1. InitComputed ()
First, during vUE initialization, computed computed properties are initialized in initState () and determined to be executed if computed properties are defined
InitComputed () method, as follows
if (opts.computed) initComputed(vm, opts.computed)
Copy the code
Take a look at this initComputed () method, which has four main steps
- Create an empty object to hold watcher
- For a computed attribute traversal, get each computed attribute defined
- Create a Watcher for each calculated property
- Evaluate each calculated property iterated. If it is not a property on the VM, execute defineComputed (). If it is a property on the VM, determine if the current calculated property name is defined in data and props
Take a look at the code and comments below
const computedWatcherOptions = { computed: true } // Instantiate the Watcher configuration item
function initComputed (vm: Component, computed: Object) {
Create empty object Watchers, vm._computedWatchers
const watchers = vm._computedWatchers = Object.create(null)
// Determine if it is server side rendering (SSR), here we analyze the browser environment
const isSSR = isServerRendering()
// Step 2. For computed attribute traversal
for (const key in computed) {
const userDef = computed[key] // Get the value of each calculated attribute defined
/ / to get the calculation of the value of the above, whether the function, if the function, direct assignment to getter, people would not calculate attribute get assigned to getter (see the website calculate attribute definitions, -- -- -- -- > sample)
const getter = typeof userDef === 'function' ? userDef : userDef.get
// Warning if the calculated property does not define a getter
if(process.env.NODE_ENV ! = ='production' && getter == null) {
warn(
`Getter is missing for computed property "${key}". `,
vm
)
}
if(! isSSR) {// Step 3. Create a Watcher for each getter (this can be called computed Watcher, and instantiated computed Watcher is analyzed below)
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. The calculated properties defined by the component are already defined on the component prototype
// Define the key, if it is not a vm attribute, and call defineComputed () for reactive processing
if(! (keyin vm)) {
defineComputed(vm, key, userDef)
} else if(process.env.NODE_ENV ! = ='production') {
// Warning if key is already defined in data, or props
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
Let’s take a look at some of the processing done in the four steps above
2. DefineComputed ()
The purpose of this method is to add getters and setters to each calculated property using Object.defineProperty () to make it responsive
// Define an Object as an argument to Object.defineProperty ()
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop, / / empty function
set: noop
}
export function defineComputed (
target: any,
key: string, // The name of the calculated property defined
userDef: Object | Function // Compute the processing done to the attribute
) {
// Add get and set handlers to the sharedPropertyDefinition object
constshouldCache = ! isServerRendering()// Non-server rendering
if (typeof userDef === 'function') { / / function
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else { / / objectsharedPropertyDefinition.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, the calculated property is processed by calling Object.defineProperty
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
In normal development scenarios, there are few setters for computing properties. We will mainly analyze getters. The above definition of get function will enter createComputedGetter () in our common browser environment. So let’s see what this function does, and keep going, okay
3. CreateComputedGetter ()
The final getter corresponds to the return value of createComputedGetter (), as follows
function createComputedGetter (key) {
return function computedGetter () {
// Get this._computedWatchers. This is the object defined on the VM above in initComputed, accessible through this, which holds the watcher for each calculated attribute. That's the Watcher for every calculated property that you get
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// Call watcher.depend() and return watcher.evaluate(). Look at the following four
watcher.depend()
return watcher.evaluate()
}
}
}
Copy the code
For the returned function, call the Depend () and evaluate () methods, which are defined in the Watcher class as follows
4. New Watcher () for calculating attributes
So Watcher is a class that’s defined, so I’m just going to take the code that defines Watcher, it’s a little bit too much, and you can look at it, and the ones that have to do with calculating properties, I’ll take them out later
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '.. /util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '.. /util/index'
let uid = 0
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.computed = !! options.computedthis.sync = !! options.syncthis.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {} process.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
/** * 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
}
/** * 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)}}}/** * Clean up for dependency collection. */
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/** * Subscriber interface. * Will be called when a dependency changes. */
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() = > {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
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
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
cb.call(this.vm, value, oldValue)
}
}
}
/** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
/** * Depend on this watcher. Only for computed property watchers. */
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
/** * Remove self from all dependencies' subscriber list. */
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)}let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)}this.active = false}}}Copy the code
When you execute initComputed () above, in what we call step 3, walk through the instantiation to create a watcher (computed Watcher) for the calculated properties
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // const computedWatcherOptions = { computed: true }
)
Copy the code
Just to simplify new Watcher, let’s see
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
// ...
if (this.computed) { // 判断this.computed
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get() // Call get() to evaluate}}Copy the code
Here you can see that the Watcher calculating the property does not evaluate immediately, but instead judges this.computed while instantiating a DEP instance.
In the process of rendering data to the page, if the vue accesses the defined calculated property, it fires the getter for the calculated property (the getter is the return value of the createComputedGetter () function) and gets the watcher for the calculated property. It then executes the Depend method in Watcher and returns the evaluate value
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
Copy the code
Depend method
/** * Depend on this watcher. Only for computed property watchers. */
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
Copy the code
Note that dep. target is rendering watcher, so this.dep.depend() corresponds to rendering Watcher subscribed to this computed Watcher change.
Then perform the watcher.evaluate () evaluation
The evaluate () function first evaluates this.dirty. If true, it calls get () to evaluate it. Get () is also defined in the Watcher class
Value = this.getter.call(vm, vm), which calls the getter defined in the calculation property, evaluates it, sets this.dirty to false, and returns the desired value
/** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
Copy the code
The important thing to note here is that since this.firstName and this.lastName are both responsive objects, their getters are fired and, according to our previous analysis, they will add their own DEP to the watcher currently being evaluated. In this case, DEP. Target is this computed watcher.
5. Modify the data that computing attributes depend on
Once we modify the data on which the calculated property depends, the setter process is triggered to notify all watcher updates subscribed to the change (dep.notify ()).
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Copy the code
As above, iterate through the loop and execute watcher’s update () method
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() = > {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
If this.dep.subs.length === 0, no one subscribed to the change of the calculated watcher. Set this.dirty to true. So that the evaluation will only be reevaluated the next time the property is accessed.
In our example scenario, rendering watcher subscribed to this computed Watcher change, then it would perform:
this.getAndInvoke(() = > {
this.dep.notify()
})
Copy the code
getAndInvoke (cb: Function) {
const value = this.get() // will recalculate
// The old and new values are compared, and if they change, the incoming callback is executed
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
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
cb.call(this.vm, value, oldValue)
}
}
}
Copy the code
The getAndInvoke function recalculates and compares the old and new values. If it changes, the callback function is executed, in this case this.dep.notify(), which in our case triggered the rendering watcher to re-render
Here to compare the calculation on properties of the old and new values, this ensures the, when calculating the value of attribute dependence of change, he will be recalculated, but not necessarily to render, only calculated the new value relative to the old value changed, will trigger the rendering watcher to render, this is essentially an optimization of the vue, much calculation, less updates
3. Source code parsing (2.6.13 version, more rendering, less calculation)
In the Update method of Watch, there is no getAndInvoke method. Instead of comparing the old and new values calculated by the calculated property, updateComponent is triggered to update the render regardless of whether they are equal or not
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); }};Copy the code
Look at the previous version (2.5.17 Beta)
Watcher.prototype.update = function update () {
var this$1 = this;
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(function () {
this$1.dep.notify(); }); }}else if (this.sync) {
this.run();
} else {
queueWatcher(this); }};Copy the code
Take a look at the getAndInvoke () method
Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
var value = this.get();
// If the calculated new value is different from the old value, the execution will continue. If the calculated new value is different, the execution will not continue
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
var oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\" ")); }}else {
cb.call(this.vm, value, oldValue); }}};Copy the code