Computed
Initialization process
Vue’s computed properties are initialized in both initState and vue.extend
In initState
export function initState (vm: Component) {
// ...
const opts = vm.$options
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
Copy the code
If vm.$options has computed, initComputed is called
// For computed Watcher, the lazy value is true
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// Create the VM._computedWatchers object
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
// Walk the attributes of computed
for (const key in computed) {
// Get the attribute value
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 a Watcher for computed
/ / create computedWatcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// When the component function (Sub) is created using the extend method, the prototype of Sub has been mounted with computed properties
// In this case, we only define the computed properties defined at instantiation time. For example, the computed properties of the root component
// Ensure that the attribute name of computed is not the same as that of data and props
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
InitComputed creates a Computed Watcher for each Computed property. The point to note here is that for Computed Watcher, options.lazy is true and the Computed property is assigned to the getter property of the Watcher instance. The created Computed Watcher is added to vm._computedWatchers. Next, defineComputed is executed to add the response
Consider the difference between Computed Watcher and Render Watcher
// The code inside the Watcher class
this.lazy = !! options.lazy// ...
this.dirty = this.lazy
// ...
this.value = this.lazy ? undefined : this.get()
Copy the code
For Computed Watcher, the lazy value is true and the dirty value is true. Because lazy is true, the this.get() method is not executed during the creation of Computed Watcher. The return value of the evaluated property will not be retrieved.
The component’s Render function is executed by executing this.get() during the creation of Render Watcher
The Vue. The extend
if (Sub.options.computed) {
initComputed(Sub)
}
Copy the code
Vue.extend performs the initComputed function
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
// Mount the computed properties of the component to the prototype of the component function
When instantiated, it can be accessed using this.key
defineComputed(Comp.prototype, key, computed[key])
}
}
Copy the code
The initComputed function executes the defineComputed method on all computed properties.
DefineComputed defined in SRC/core/instance/state. Js
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// True if not SSR
constshouldCache = ! isServerRendering()/* * userDef may be a function or an object with getter and setter properties */
// Set the fetching descriptor
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
// Set the saving descriptor
sharedPropertyDefinition.set = userDef.set || noop
}
// An error is reported when assigning a value to a evaluated property if no setter method is set for the evaluated property key
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)}}// Add interception to allow developers to access this.key(vm.key)
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
The defineComputed method adds all computed properties to the VM/sub.prototype via object.defineProperty and sets the return value of the createComputedGetter function to take the descriptor; Sets the set method that evaluates the property as a storage descriptor
Now let’s look at createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {}}Copy the code
Look at the internal logic of the createComputedGetter function in a moment, but for now it returns a function that is fired when the evaluated property is retrieved
summary
componentcomputed
The initialization
For component computed initialization, all computed properties in the component are added to the prototype Object of the component constructor using the object.defineProperty method and access descriptors are set when the component constructor is created.
When a component instance is created, a Computed Watcher is created for each Computed property and the Computed property is copied to the getter property of the Watcher instance. In the development environment, the system determines whether the key in computed is the same as the key in data and props.
The root instancecomputed
The initialization
To initialize the root instance computed, it is easy to get the computed properties, create a computed Watcher for each key of computed, and mount all the computed properties to the component instance using the object.defineProperty method. And set the access descriptor.
Principle of response
When the component executes the render function, if a computed property is used, it fires the getter for that computed property, which is the return value in the createComputedGetter above
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// Do the dependency collection only once
if (watcher.dirty) {
// Execute the defined computed property function
watcher.evaluate()
}
if (Dep.target) {
// Add the render watcher to the deP of the dependency property. When the dependency property is changed, trigger the component update through the Render watcher get method
watcher.depend()
}
return watcher.value
}
}
}
Copy the code
First, a Computed Watcher is obtained based on the key. Because watcher.dirty is true during initialization, the watcher.evaluate() method is executed
evaluate () {
this.value = this.get()
this.dirty = false
}
Copy the code
The evaluate method executes the this.get() method, gets the return value of the evaluated property, and sets the current Watcher’s dirty to false, preventing multiple executions of the this.get() method.
The GET method was seen in the Principles of Responsiveness section
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
// ...
popTarget()
this.cleanupDeps()
}
return value
}
Copy the code
For a Computed Watcher, run this. Getter to compute the attribute value and obtain the result value. Then, exit the stack, add the deP of the dependent attribute to depIds and deps, and return the result.
When this. Getter is executed, the variable value of the dependent property in the Computed property is obtained, and the getter of the responder variable is triggered to add Computed Watcher to the dep.subs of the responder variable.
Return to createComputedGetter, where dep. target refers to the component’s Render Watcher, because the component’s Render Watcher is pushed when the component’s Render function is executed. When the value of the Computed attribute is obtained, the compute Watcher is pushed into the stack and Computed Watcher is removed from the stack. Therefore, Dep. Target refers to the Render Watcher of the component. Next, execute the Depend method of Computed Watcher
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
Copy the code
The depend method iterates over deps, which is an array of Dep instances, and executes the depend method for each Dep instance, adding the component Render Watcher to the dep.subs of all the attributes that the attribute depends on
Watcher. depend returns the return value of the evaluated property after execution, and the collection of dependencies is complete
summary
The dependency collection process for computing attributes is really a dependency collection process for the responsive attributes that are used
Computed Watcher is added to the dep.subs of responsive properties during the execution of the Computed property when it is used in the component’s Render function. Add the component’s Render Watcher to the responsive property dep.subs as well
update
The setter method for the dependent property is triggered when a responsive property change for a property dependency is evaluated
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
Copy the code
The setter method notifies all Watcher, including Computed Watcher and Render Watcher, of updates. Call Watcher’s update method
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
For Computed Watcher, set dirty to true. Render Watcher executes the run method of the Watcher instance, re-executing the component’s Render function to update the return value of the evaluated property
The effect of dirty is to reevaluate only when the relevant reactive properties change. If the return value of a evaluated property is repeatedly retrieved, it will not be re-evaluated as long as the responsive property has not changed.
That is, when the responsive property changes, the setter of the responsive property is triggered and the dirty value is set to false for Computed Watcher. When obtained again, the latest value is obtained and the Watcher is added to the dep.subs of the responsive property
Watch
The initialization of watch also takes place in initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
/ /...
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
Add an _Watchers array to the VM to hold the current component’s watch. The initWatch method is then called to initialize all watch properties
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
InitWatch calls createWatcher for all watches
function createWatcher (
vm: Component,
expOrFn: string | Function, handler: any, options? :Object
) {
if (isPlainObject(handler)) {
// Processing parameters
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
// Handler can be a method name
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Copy the code
CreateWatcher takes the callback function and calls the vm.$watch method
Vue.prototype.$watch = function (
expOrFn: string | Function, cb: any, options? :Object
) :Function {
const vm: Component = this
// If the listener is set by this.$watch, then createWatcher is executed to get the callback function
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// If user is true, the Watcher created is a user Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
// If options. Immediate is true, the callback function is executed immediately
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}// Return a function to cancel the listener
return function unwatchFn () {
watcher.teardown()
}
}
Copy the code
Value.prototype.$watch: create a User Watcher and check whether options. Immediate is true. Finally, a function is returned to cancel the listening.
This is the end of the watch initialization process
summary
The ultimate goal of the watch initialization process is to create a User Watcher for each watch. During the creation process, the monitored properties are collected (described in the next section).
Watch the update
During initialization, a User Watcher will be created for each watch, and the dependency collection of the monitored properties will be done during the creation
const watcher = new Watcher(vm, expOrFn, cb, options)
Let’s look at the parameters first
Name of the attribute monitored (XXX,'xxx.yyy'Options {user:true, deep: [custom configuration item], async: [custom configuration item]}Copy the code
Procedure for creating a User Watcher
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.vm = vm
if (isRenderWatcher) {
// If Watcher is not rendered, _watcher will not be mounted to the VM
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
/** * computedWatcher lazy is true * userWarcher user is true * deep and sync are configuration items of watch */
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// User Watcher's expOrFn is a string representing the name of the property monitored
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop 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
)
}
}
// For computed Watcher, the lazy attribute is true. That is, the GET method is not executed immediately
Render Watcher's lazy property is false, and the get method is immediately executed, returning undefined
// If the user Watcher's lazy property is false, the user Watcher will immediately execute the get method and return the value of the property being listened on
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
User Watcher has two properties, deep and async, in addition to the true User property. These two properties are the configuration items of watch.
A given Watcher is instantiated to determine the type of the expOrFn parameter. For User Watcher, the expOrFn is the name of the property listened on, which is a string, so the parsePath method is executed.
export function parsePath (path: string) :any {
if (bailRE.test(path)) {
return
}
const segments = path.split('. ')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
The parsePath method is based on. Cut the string into an array of strings and return a function that is assigned to the User Watcher’s getter property. Internally, the function gets the attribute values of all the elements in the array in turn and returns the attribute values
Assuming the name of the property to be listened on is A.B.C, this function will fetch the values of this.a, this.a.b, and this.a.b.c in sequence
Go back to User Watcher, where the this.getter has been assigned, and the this.get method is executed; That is, only Computed Watcher is created without the this.get method
Look at the get method again
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {} finally {
if (this.deep) {
// If deep is true and the property being listened on is an object, then all properties in the object are collected once
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
Copy the code
The logic of the get methods is the same whether they calculate properties, data, props, and watch.
- The current
Watcher
Into the stack - perform
this.getter
(Each type ofWatcher
thegetter
Properties of different) - perform
traverse
Methods (onlydeep
fortrue
theUser Watcher
Will perform) - The current
Watcher
Out of the stack - To deal with
Watcher
thedeps
attribute - return
value
In the case of User Watcher, the getter is the return value of parsePath. During the execution of the getter, it will get the value of the property being listened on, which will trigger the getter method of the property being listened on, adding User Watcher to the property’s dep.subs.
After the above execution, the traverse method is executed to determine if deep is true. If true, the traverse method is executed
const seenObjects = new Set(a)export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
Copy the code
Traverse methods are simple. If the property being listened to is an object, they call all the properties of the object, triggering dependency collection for all the properties. Add User Watcher to the dep.subs for each property so that when a property is modified, the setter for the property is triggered, triggering the watch callback
Triggered the callback
When the value of the property is changed, the setter for the property is triggered, notifies all watchers in dep.subs of the update, and executes the watcher.update method
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
If User Watcher’s sync property is true, the run method is executed immediately. If sync is false, the run method of User Watcher is executed in the next task queue using queueWatcher(this)
Take a look at the run method first
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
isObject(value) ||
this.deep
) {
// Why you can get old and new values in the parameters of the callback function when adding a custom watcher
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
For User Watcher’s run method, it first calls this.get() to reset the dependency collection for the monitored property and get the latest value. If the latest and old values do not want to wait, the callback function is called and the new and old values are passed in
The above judgment logic in addition to determine whether the old and new values to will determine isObject (value) | | this. Deep, this is because if the monitor properties is an object/array, modify the object/array properties, old and new values are the same, so in order to prevent this kind of situation leads to the callback does not perform, To add this logic
Let’s see how User Watcher is called in queueWatcher; Normally, being identical with data is to add User Watcher to the queue and ensure that each User Watcher in the same queue is unique
Different situation
inwatch
Callback to modify the value of another property being listened on
His execution logic is as follows:
export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) = > a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// Execute the component's beforeUpdate hooks, with parents before children
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break}}}Copy the code
FlushSchedulerQueue method is executed on the next queue
flushing
Set totrue
Is being updated in the queueWatcher
;- Queue sort, guarantee
Watcher
Update order; - Traverse the queue, updating all in the queue
Watcher
has[id] = null
, will be updatedWatcher
fromhas
Removed;- perform
User Watcher
therun
Methods; - Do the first one
watch
In which the value of the property being listened on is modified and the property is triggeredsetter
Will listen for this propertyUser Wather
throughqueueWatcher
To the queue, and now it’s different
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if(! flushing) { queue.push(watcher) }else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// queue the flush
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
Because I already set flushing to true up here, so I’m going to do else logic; The else logic is to walk through the queue and add Watcher to the corresponding location. The location logic is as follows
1. Component updates are from parent to child. (because the parent component is always created before the child component) 2. User Watcher executes 3. If a component is destroyed during a parent Watcher execution, its Watcher execution can be skipped, so the parent Watcher should be executed firstCopy the code
NextTick (flushSchedulerQueue) is not executed again because waiting is already true. Instead, we return to the flushSchedulerQueue method to continue the loop
inwatch
Callback to modify the value of the currently monitored property
FlushSchedulerQueue: flushSchedulerQueue: flushSchedulerQueue: flushSchedulerQueue
Because the newly added User Watcher is the same Watcher as the User Watcher that has just been executed, the if condition that is triggered next is true in the development environment
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break}}Copy the code
In this case, circular[ID] will increase the number of circular[ID] by one and repeat the preceding logic until the total number is greater than MAX_UPDATE_COUNT
conclusion
Difference between Computed and watch
computed
- Depends on other attribute values, and
computed
The value is cached - Get it next time when the value of the property it depends on changes
computed
It will be recalculated when the value is givencomputed
The value of the
watch
- No caching, more observation
- Each time the monitored data changes, a callback is performed for subsequent operations
Usage scenarios
- Should be used when numerical calculations are required and other data are dependent
computed
Because it can be usedcomputed
To avoid having to recalculate a value every time it is fetched - Used when you need to perform asynchronous or expensive operations as data changes
watch
, and before getting the final result, you can set the intermediate state
For computed
During initialization, a Computed Watcher is created for each Computed property, all Computed properties are added to the prototype Object of the component instance/component constructor via object.defineProperty, and access descriptors are added to all Computed properties.
When a computed property is obtained, the getter of the computed property is triggered to calculate the value computed and the dirty is set to false. In this way, the cached value is directly returned when the computed property is obtained again. During the computation of a computed value, a computed Watcher is added to the Dep of the dependent attribute.
When the dependent attribute changes, the update of Computed Watcher is triggered. The dirty value is set to true, and the Computed value is recalculated the next time the dependent attribute is obtained
How does watch trigger a callback
During initialization, a User Watcher is created for each watch. If the watch’s immediate value is true, a callback is performed immediately. When User Watcher is created, the value of the property to be listened on is fetched once, which triggers the getter method of the property to be listened on, and adds User Watcher to the Dep instance of the property to be listened on.
When the monitored property changes, the User is notified that the Watcher is updated. If sync of the watch is true, the watch callback is immediately executed. Otherwise, the update method of User Watcher will be placed in the cache queue by nextTick. In the next event loop, the property value of the monitored property will be retrieved, and the new value will be determined whether the new value wants to wait, whether the deep value is set to true, and whether the monitored property is an object type. If so, the callback will be performed.