Editor’s note: From time to time, we invite engineers to talk about interesting technical details in the hope that knowing why will help you perform better in an interview. It also gives the interviewer more ideas.
Although the current technology stack has been transferred from Vue to React, the actual experience of several projects developed using Vue before is very pleasant. Vue documents are clear and standardized, API design is simple and efficient, friendly to front-end developers, and easy to use. I even think it is more efficient to use Vue than React in many scenarios. I have read the source code of Vue on and off before, but I haven’t summarized it. Therefore, I will make some technical summaries here to deepen my understanding of Vue. So today I’m going to write about the implementation of computed, one of the most commonly used apis in Vue.
Basic introduction
Without further ado, a basic example is as follows:
<div id="app">
<p>{{fullName}}</p>
</div>
Copy the code
new Vue({
data: {
firstName: 'Xiao'.lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Copy the code
In Vue we don’t need to evaluate {{this.firstName + “+ this.lastName}} directly in the template, because putting too much declarative logic in the template would make the template too heavy, especially if the page uses a lot of complex logical expressions to process data. This has a big impact on the maintainability of a page, and computed is designed to solve this problem.
Contrast listenerwatch
Of course, a lot of times when we use computed, we compare it to another API in Vue, the watch listener, because in some ways it’s the same, it’s based on Vue’s dependency tracking mechanism, and when a dependency data changes, All related data or functions that depend on this data are automatically changed or called.
While computing properties is more appropriate in most cases, sometimes a custom listener is required. That’s why Vue provides a more generic way to respond to changes in data with the Watch option. This approach is most useful when asynchronous or expensive operations need to be performed when data changes.
As we can see from the official Vue documentation explaining watch, using the Watch option allows us to perform asynchronous operations (accessing an API) or high-performance operations, limits how often we can perform that operation, and sets intermediate states before we get the final result, all of which cannot be done by computing properties.
The following also summarizes several points aboutcomputed
和 watch
Difference:
computed
Is to evaluate a new attribute and mount it to the VM (Vue instance), whilewatch
Yes The listener already exists and is mounted tovm
The data of thewatch
You can also listen incomputed
Calculate property changes (among othersdata
,props
)computed
Essentially a lazy-evaluated observer, cacheable only when the dependency changes after the first accesscomputed
Property, the new value will be computed, andwatch
The execution function is called when the data changes- In terms of usage scenarios,
computed
Applies to one data affected by multiple data, whilewatch
Apply one data to affect multiple data;
Above, we have seen some differences between computed and Watch and the differences in usage scenarios. Of course, sometimes the two are not so clear and strict, and finally, we need to analyze them in different businesses.
The principle of analysis
To get back to computed, the topic of this article, let’s take a closer look at how it works in the Vue source code.
Before looking at computed source code, we need to have a basic understanding of Vue’s responsive system, which Vue calls a non-invasive responsive system. The data model is just plain old JavaScript objects, and the view updates automatically when you modify them.
When you pass a normal JavaScript Object to the Vue instance’s data option, Vue iterates through all of the Object’s properties and converts them into getters/setters using Object.defineProperty. These getters/setters are invisible to the user, but internally they let Vue track dependencies and notify changes when properties are accessed and modified. Each component instance has a corresponding Watcher instance object, which records properties as dependencies during component rendering. Later, when the setter for the dependency is called, Watcher is told to recalculate, causing its associated component to be updated.
Vue response system has three core points: Observe, Watcher and DEP:
observe
: traversaldata
Property in theObject.defineProperty 的get/set
Methods Data hijacking was carried out.dep
: Each property has its own message subscriberdep
Is used to hold all observer objects subscribed to the property.watcher
: Observer (object), passdep
The implementation of the response properties to listen, listen to the result, actively trigger their own callback to respond.
With a basic understanding of responsive systems, let’s look at computational properties. First we find calculate attribute is initialized in the SRC/core/instance/state. The js file initState function
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 initialization
if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
Call the initComputed function (which also initializes initData and initWatch, respectively) and pass in two parameters, the VM instance and the computed option defined by the opt.computed developer, to initComputed:
const computedWatcherOptions = { computed: 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
Starting with this code, let’s look at the following sections:
-
Gets the definition userDef and getter evaluation functions for the calculated property
const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get Copy the code
There are two ways to define a calculated property: one is to add a function directly, and the other is to add the object form of set and get methods, so we first get the definition of the calculated property userDef, and then get the corresponding getter function according to the type of userDef.
-
Calculate the observer watcher and message subscriber DEP for the properties
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) Copy the code
The reference to vm._computedWatchers object contains watcher instances of each calculated attribute. The watcher constructor takes four arguments to instantiate the watcher constructor: The vm instance, getter evaluation function, noOP empty function, computedWatcherOptions constant object (here Watcher is given an identifier {computed:true} indicating that this is a computed property and not a non-computed observer, We come to the definition of the Watcher constructor:
class Watcher { constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {if (options) { this.computed = !! options.computed }if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { popTarget() } return value } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke((a)= > { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } depend () { if (this.dep && Dep.target) { this.dep.depend() } } } Copy the code
For brevity and focus, I’ve manually removed the code snippets that we don’t need to worry about for now. Look at Watcher’s constructor, using the fourth parameter {computed:true} passed in by new Watcher, For evaluating the property, Watcher executes the if condition this.dep = new dep (), which is the message subscriber that created the property.
export default class Dep { statictarget: ? Watcher; subs:Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null Copy the code
Dep also simplifies some of the code. Let’s look at the relationship between Watcher and Dep and summarize it in one sentence
The DEP is instantiated in watcher and subscribers are added to dep.subs, which notifies each Watcher of updates through notify through dep.subs.
-
DefineComputed Defines compute attributes
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
Because computed attributes are mounted directly to instance objects, you need to determine whether an object already has a property with the same name before defining it. DefineComputed passes in three parameters: the VM instance, the key to calculate the property, and the definition of the calculated property (object or function) by userDef. Then continue to find the defineComputed definition:
export function defineComputed (target: any, key: string, userDef: Object | Function) { constshouldCache = ! isServerRendering()if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else{ sharedPropertyDefinition.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)}}Object.defineProperty(target, key, sharedPropertyDefinition) } Copy the code
At the end of this code, the native Object.defineProperty method is called, where the third argument passed in is the property descriptor sharedPropertyDefinition, initialized as:
const sharedPropertyDefinition = { enumerable: true.configurable: true.get: noop, set: noop } Copy the code
The get/set method of sharedPropertyDefinition is overwritten after userDef and shouldCache. The get function of sharedPropertyDefinition is the result of createComputedGetter(key), We find the createComputedGetter call result and finally rewrite the sharedPropertyDefinition to look something like this:
sharedPropertyDefinition = { enumerable: true.configurable: true.get: function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } }, set: userDef.set || noop } Copy the code
The get access function is executed when the calculated property is called to associate the watcher with the observer object and then execute Wather.depend () to collect the dependency and watcher.evaluate() to evaluate.
After analyzing all the steps, let’s summarize the whole process:
- When the component is initialized,
computed
和data
Will build their own response systems,Observer
traversedata
For each property set inget/set
Data interception - Initialize the
computed
Will be calledinitComputed
function- Sign up for a
watcher
Instance, and instantiate one insideDep
Message subscribers are used for subsequent collection dependencies (such as for rendering functions)watcher
Or something else to observe changes in the properties of the calculationwatcher
) - This is triggered when the calculated property is invoked
Object.defineProperty
theget
Accessor function - call
watcher.depend()
Method to its own message subscriberdep
的subs
To add additional attributeswatcher
- call
watcher
的evaluate
Method (then calledwatcher
的get
Let yourself be something elsewatcher
Subscribers to the message subscriber will firstwatcher
Assigned toDep.target
And then executegetter
Evaluation functions, when accessing properties inside the evaluation function (such as fromdata
,props
Or othercomputed
), will also trigger themget
The accessor function then computes the propertywatcher
Attribute added to the evaluation functionwatcher
Message subscriber todep
When these operations are complete, they are finally closedDep.target
Assigned tonull
And returns the result of the evaluation function.
- Sign up for a
- Trigger when a property changes
set
Intercepting the function and then calling its own message subscriberdep
的notify
Method to traverse the currentdep
Holds all subscribers inwathcer
的subs
Array and call them one by onewatcher
的update
Method to complete the response update.
The text/versa
A coder yearning for poetry and the distance
Sound/fluorspar
This article has been authorized by the author, the copyright belongs to chuangyu front. Welcome to indicate the source of this article. Link to this article: knownsec-fed.com/2018-09-12-…
To subscribe for more sharing from the front line of KnownsecFED development, please search our wechat official account KnownsecFED. Welcome to leave a comment to discuss, we will reply as far as possible.
Thank you for reading.