preface
Computed is a very common attribute configuration in Vue, and it is very convenient for us to change with dependent attributes. So this article gives you a full understanding of computed internals and how it works.
Before I do that, I hope you have some understanding of the responsive principle, because computed works on the responsive principle. If you are not familiar with the responsivity principle, you can read my previous article: Hand Touch guide to Understanding the Responsivity principle of Vue
The computed usage
To understand a principle, the most basic thing is to know how to use it, which is helpful for later understanding.
First, function declarations:
var vm = new Vue({
el: '#example'.data: {
message: 'Hello'
},
computed: {
// Calculates the getter for the property
reversedMessage: function () {
// 'this' points to the VM instance
return this.message.split(' ').reverse().join(' ')}}})Copy the code
Second, object declaration:
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]}}}Copy the code
Note: Data attributes used in computed data are collectively referred to as “dependent attributes”
The working process
Let’s take a look at how computations work in general, and see what the core point of a computed attribute is.
Entry file:
/ / source location: / SRC/core/instance/index, js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
Copy the code
_init
:
/ / source location: / SRC/core/instance/init. Js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? : Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions merges the mixin option with the options passed in by new Vue
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// Initialize the data
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Copy the code
initState
:
/ / source location: / 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 */)}// Computed is initialized here
if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
initComputed
:
/ / source location: / SRC/core/instance/state. Js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
/ / 1
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]
/ / 2
const getter = typeof userDef === 'function' ? userDef : userDef.get
if(! isSSR) {// create internal watcher for the computed property.
/ / 3
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true})}// 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)) {
/ / 4
defineComputed(vm, key, userDef)
}
}
}
Copy the code
- Instantiated
_computedWatchers
Object for storing the calculated propertiesWatcher
“ - Of the computed property
getter
You need to decide whether it is a function declaration or an object declaration - Create compute properties
Watcher
“,getter
Passed as a parameter, it is called when a dependent property is updated and revalues the evaluated property. Need to pay attention toWatcher
的lazy
Configuration, which is the identity that implements caching defineComputed
Data hijacking of computed properties
defineComputed
:
/ / source location: / SRC/core/instance/state. Js
const noop = function() {}
/ / 1
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop,
set: noop
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
// Determine whether to render for the server
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
/ / 2
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
/ / 3sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
/ / 4
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
sharedPropertyDefinition
Is the property description object that computes the property initially- When you use a function declaration, set the properties describing the object
get
和set
- When you use an object declaration, set the properties that describe the object
get
和set
- Data hijacking of calculated properties,
sharedPropertyDefinition
Passed in as the third parameter
The client render creates the GET using createComputedGetter, and the server render creates the GET using createGetterInvoker. There is a big difference between the two. Server-side rendering does not cache computed attributes, but evaluates them directly:
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this.this)}}Copy the code
But we usually talk more about client-side rendering, so let’s look at the implementation of createComputedGetter.
createComputedGetter
:
/ / source location: / SRC/core/instance/state. Js
function createComputedGetter (key) {
return function computedGetter () {
/ / 1
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/ / 2
if (watcher.dirty) {
watcher.evaluate()
}
/ / 3
if (Dep.target) {
watcher.depend()
}
/ / 4
return watcher.value
}
}
}
Copy the code
This is the core of the implementation of a calculated property, and a computedGetter is the get triggered by a calculated property when data hijacking is performed.
- In the above
initComputed
Function, “evaluates attributesWatcher
“Is stored in the instance_computedWatchers
Here extract the corresponding “calculated attribute”Watcher
“ watcher.dirty
Is the trigger point for implementing the compute attribute cache,watcher.evaluate
Reevaluate the calculated property- Rely on property collection “render
Watcher
“ - The evaluated property stores the value in
value
,get
Returns the value of the calculated property
Calculate the property cache and update
The cache
Let’s split the createComputedGetter and examine their individual workflows. This is the cache trigger:
if (watcher.dirty) {
watcher.evaluate()
}
Copy the code
Let’s look at the Watcher implementation:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
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
// options
if (options) {
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
// The initial value of dirty is equal to lazy
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.value = this.lazy
? undefined
: this.get()
}
}
Copy the code
Remember to create “compute property Watcher” with lazy set to true. The initial value of dirty is the same as lazy. So when you initialize the page rendering and evaluate the evaluated property, you do watcher.evaluate once.
evaluate() {
this.value = this.get()
this.dirty = false
}
Copy the code
Value is evaluated and assigned to this.value, where the watcher.value in the createComputedGetter above is updated. Evaluate is not executed the next time it is evaluated. Instead, watcher.value is returned.
update
When a dependent property is updated, dep.notify is called:
notify() {
this.subs.forEach(watcher= > watcher.update())
}
Copy the code
Then execute watcher.update:
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
Since the “compute attribute Watcher” has lazy true, dirty will be set to true. When the page rendering evaluates the calculated property and the trigger point condition is met, perform watcher.evaluate re-evaluation and the calculated property is updated.
Dependency properties collect dependencies
Collect calculated properties Watcher
When initialized, the page rendering pushes the “Render Watcher” onto the stack and mounts it to dep.target
Evaluate the value of an evaluated property encountered during page rendering, so execute the watcher.evaluate logic and then call this.get:
get () {
/ / 1
pushTarget(this)
let value
const vm = this.vm
try {
/ / 2
value = this.getter.call(vm, vm) // Evaluate attributes
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
Copy the code
Dep.target = null
let stack = [] // Store the watcher stack
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
export function popTarget(){
stack.pop()
Dep.target = stack[stack.length - 1]}Copy the code
PushTarget = ‘render Watcher, calculate Watcher’; pushTarget = ‘render Watcher, calculate Watcher’;
Enclosing the getter to calculate attribute evaluation in rely on attributes, trigger based on attribute data hijacked a get, execute dep. Depend dependent on collection (” calculate attribute Watcher “)
Collect render Watcher
After this. Getter is evaluated, popTragte “Calculate property Watcher” is removed from stack, dep. target is set to “Render Watcher”, dep. target is “Render Watcher”.
if (Dep.target) {
watcher.depend()
}
Copy the code
Watcher. Depend Collect dependencies:
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
Copy the code
The DEPS stored in dePS are attribute-dependent DEPs. This step is the dependency collection dependency (” Render Watcher “)
After the above two dependency collections, the subs of the dependent property store two Watcher, [calculate the property Watcher, render the Watcher]
Why do dependent properties collect render Watcher
When I read the source code for the first time, I was surprised to find that the “calculated property Watcher” was just fine. Why collect “render Watcher” depending on attributes?
The first scenario involves using both dependent and computed properties in a template
<template>
<div>{{msg}} {{msg1}}</div>
</template>
export default {
data() {return {
msg: 'hello'
}
},
computed:{
msg1() {return this.msg + ' world'}}}Copy the code
The template is useful for dependency properties, which store “render Watcher” when the page renders the value of dependency properties, so the Watcher. Depend step is collected repeatedly, but the Watcher is de-duplicated internally.
This is why I have a question. Vue is a great framework and has a point. So I thought of another scenario that would explain watcher. Depend.
Second scenario: only computed attributes are used in the template
<template>
<div>{{msg1}}</div>
</template>
export default {
data() {return {
msg: 'hello'
}
},
computed:{
msg1() {return this.msg + ' world'}}}Copy the code
Dependency properties are not used on the template, so dependency properties do not collect “render Watcher” when the page is rendered. In this case, only “calculated Watcher” will be displayed in the dependency property. When the dependency property is modified, only the update of “calculated Watcher” will be triggered. An update to the calculated property simply sets dirty to true and does not evaluate immediately, so the calculated property will not be updated.
So you need to collect “render Watcher” and execute “Render Watcher” after executing “Calculate property Watcher”. The page renders the evaluated property and executes Watcher. Evaluate to recalculate the evaluated property and update the page evaluated property.
conclusion
The principle of calculating attributes and the principle of responsivity are almost the same, the same is the use of data hijacking and dependency collection, but the difference is that the calculation of attributes has cache optimization, only when the dependent attributes change will be re-evaluated, other cases are directly returned cache value. The server does not calculate the property cache.
A “render Watcher” is required to calculate a property update, so at least two Watchers are stored in subs that depend on the property.