preface
For calculating attributes, look at the documentation on the official website:
Computed properties are cached based on their reactive dependencies and are reevaluated only when the associated reactive dependencies change
So how to interpret this sentence? Let’s start with a simple example
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>computed</title>
<script src=".. /.. /dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>{{compA}}</div>
</div>
<script>
const vm = new Vue({
el: '#app'.data: {
a: 1,},computed: {
compA() {
return this.a + 1}}})</script>
</body>
</html>
Copy the code
Looking at the source code, let’s look at how computed does for the first time.
For the first time to perform
- Initialize the
computed
When we do _init with new Vue, we’re going to do initState. This is where most of the key options are initialized and will be mounted to $options so we can get them through this.$options
export function initState (vm: Component) {
// This array will be used to store all the Watcher objects for this component instance
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 {
// If no object exists, observe empty object
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
Let’s focus on how initComputed performs.
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
// Create an empty object with the prototype null // calculate the attribute observer list
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
/** * computed: { someComputedProp () { return this.a + this.b } } */
for (const key in computed) {
const userDef = computed[key]
// Get getter object writing is different from function writing
const getter = typeof userDef === 'function' ? userDef : userDef.get
// There is no getter for the calculated property
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.
// Calculate the attributes of the observer object
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 key of the compute attribute cannot exist in props data, because it is also placed under the VM
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)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
Copy the code
It’s very clear that the entire code is divided into three pieces
- To create a
watchers = vm._computedWatchers = null
- get
computed
The method inside is assigned togetter
And thennew Watcher
And assign a value towatchers[key]
- perform
defineComputed
Method, this is going to tell youcomputed
thekey
Is here or notvm
In, an error is reported if there is a definition
Good understanding of these three steps, we can analyze step by step, not the first step. Go straight to step two
Create a calculated property Watcher
Why is it called calculated property Watcher?
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
Copy the code
export default class Watcher {
constructor (
vm: Component,
// Evaluate the expression
expOrFn: string | Function./ / callback
cb: Function./ / options
options?: ?Object.// Whether to render watcherisRenderWatcher? : boolean) {
// options
if (options) {
this.deep = !! options.deep// Whether to use depth observation
this.user = !! options.user// Identifies whether the current observer instance object is developer-defined or internally defined
this.lazy = !! options.lazy// Lazy Watcher does not request the first time
this.sync = !! options.sync// Whether to evaluate and perform callbacks synchronously when data changes
this.before = options.before // Call the callback before triggering the update
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb / / callback
this.id = ++uid // uid for batching
this.active = true // Activate the object
this.dirty = this.lazy // for lazy watchers
// Implement to avoid duplicate dependencies
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// process the expression obj.a
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
// Watcher only accepts simple points (.) Separate paths if you want to directly observe a function with all the syntax features of JSprocess.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// Attribute constructors are evaluated without value
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
In new Watcher, it passes four parameters. First, the fourth parameter, computedWatcherOptions, is an options. Let’s see what this thing is
const computedWatcherOptions = { lazy: true }
Copy the code
Assign lazy to true, and then set this.dirty = this.lazy = true in the Watcher constructor. It has different attributes than the Wacher defined in mountComponent, so use its options name to evaluate the Wacher.
Notice that because it’s lazy = true, the first time it executes, it’s not going to evaluate, so it’s not going to execute. Enclosing the get (). That by itself, the comP in our example does not participate in watcher’s evaluation. Pay attention and think about why you’re doing this.
defineComputed
Ok, let’s look at the next method defineComputed(VM, key, userDef), which, as is clear from its name, defines the interceptor
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// Compute the attribute cup cache under the non-server
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
// The browser side and the server side handle it differently
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
}
// Computed cannot be assigned when the computed property is not set to SET
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
We just have to look at what the sharedPropertyDefinition uses get and set.
- when
userDef
Is a method used in non-server rendering environmentscreateComputedGetter(userDef)
And,set
Is an empty function - when
userDef
Is an objectcreateComputedGetter(userDef.get)
And theset
The assignment is performed
So what does the createComputedGetter do
function createComputedGetter (key) {
// Compute the property interceptor
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// dirty = lazy = true
This. get makes a dependency on the data value of the method in the calculation property
// Evaluate the watcher
if (watcher.dirty) {
watcher.evaluate()
}
/ / render watcher
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
Copy the code
The createComputedGetter(key) method is executed and a computedGetter is returned. We define get as a computedGetter method and use closures to cache it. According to the example defineComputed, the result is
Object.defineProperty(vm, 'compA', {
configurable: true.enumerable: true.get: computedGetter(),
set: () = >{},})Copy the code
We created an interceptor for compA on our instance. So initComputed is done.
Generate the execute function
We’re not going to go into the details here, because it’s a long one to make clear, but the brief process is to run up to the creation of the render Wacher
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {... updateComponent =() = > {
vm._update(vm._render(), hydrating)
}
...
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
return vm
}
Copy the code
Lazy is false, so this.get() is called at the end of the constructor initialization
get () {
...
value = this.getter.call(vm, vm)
...
return value
}
Copy the code
Without looking at the rest of the code, the core is to execute this.getter, which is assigned in the constructor and is called updateComponent. So we executed vm._update(vm._render(), hydrating). First look at the vm._render method, which is defined in render.js. Now it’s all about two lines of code.
Vue.prototype._render = function () :VNode {
const vm: Component = this
// Get the render function from the $options after the formatting
const { render, _parentVnode } = vm.$options
let vnode
// Execute render. Call (vm, get user input or compile generated createElement function)
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
Copy the code
- from
vm.$options
To deriverender
. - perform
render
Methods. Pass in the current instance, andvm.$createElement
.vm.$createElement
isinitRender
Time definedcreateElement
Let’s see what the render is, this is going to platforms/web/entry – the runtime – with – the compiler look down. This is a context-dependent value
// Cache the $mount method in runtime/index.js
const mount = Vue.prototype.$mount
// Override vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
// Get the dom of the mount point
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
// Use the template or el option to build the render function
if(! options.render) {let template = options.template
... // Get the HTML of the template. The HTML generated by el is assigned to the template
// There is a possibility that template is empty
if (template) {
// Generate render and static render via compileToFunction
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV ! = ='production'.// Handle property newlines during template compilation
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
/ / call the mount
return mount.call(this, el, hydrating)
}
Copy the code
Here we get render and assign it to this.$options. So that’s why we can get it up here, so let’s go in and look at compileToFunctions, layer by layer, and here we are
export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
// Parse the original code and generate the AST
const ast = parse(template.trim(), options)
if(options.optimize ! = =false) {
// Mark immutable code in ast as static
optimize(ast, options)
}
// Generate code for the target platform based on the given AST
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
Copy the code
Here is the core of parsing HTML, in which Vue uses the method of function currification to isolate parsing and some error judgment, we hit the debugger after code. Look directly at the final generation of the template
with(this){
return _c('div', {attrs: {\"id\":\"app\"}},[_c('div',[_v(_s(compA))])])
}
Copy the code
So this is the code string for our example, and then we get an anonymous Function with new Function(code)
; (function anonymous () {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [_c('div', [_v(_s(compA))])])
}
})
Copy the code
Executing anonymous functions
Note here that vm._renderProxy is a proxy proxy, or VM, in the development environment. In a formal environment it is the VM.
// vue.prototype. _init
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production') {
// Set the scope proxy for the render function. The purpose is to give us better hints
initProxy(vm)
} else {
// In the production environment, the template compiled by vue-loader is js code that is not wrapped with the with statement.
// There is no need for proxy has to delegate with
vm._renderProxy = vm
}
Copy the code
Ok, so let’s do that, so first of all let’s figure out what _c, _v, and _s are. _c we can see in initRender, which is createElement and returns a Vnode. You can also see this in instance/render-helpers/index.js.
/ / return Vnode
_c = createElement(vm, a, b, c, d, false)
// Create an empty node
_v = createTextVNode
// Convert to a string
_s = toString
Copy the code
We’re looking at the toString execution of compA, where we run this[‘compA’]. Because the interceptor is defined above, we will execute to the computedGetter.
Start real computed implementation
Perform watcher. The evaluate ()
Because we cached the previous key, we got the computed Watcher of the previous new under the current instance. For the first time, we execute watcher. Evaluate (). Note that watcher evaluates watcher.
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// dirty = lazy = true
This. get makes a dependency on the data value of the method in the calculation property
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
Copy the code
At this point we execute this.get(). From the above analysis, executing this.get() is executing this.getter, that is, initializing an evaluation expression passed in. Computed evaluation expressions are
evaluate () {
this.value = this.get()
this.dirty = false
}
Copy the code
You can clearly see that at this point we trigger this.a. Based on the reactive principle, here we will trigger the interceptor GET defined in data. To collect reactive dependencies, we put our computational Watcher in dep.subs. I won’t go into the details of the reactive collection of data here. Wait for the next article on data analysis. All we need to know is that a’s DEP array holds the computational Watcher. And then I put dirty = false. Watcher.evaluate () executes.
function () {
return this.a + 1
}
Copy the code
Perform watcher. Depend ()
Before we execute watcher.depend(), what is dep.target?
First, we need to make it clear that this is a global property that points to the current Watcher during initialization. Now we are doing the initialization of the entire Vue, which has one and only Watcher, rendering Watcher
So why does he become the current render Watcher? Here we need to go back to the above implementation this.get(). Look at the code
get () {
// Assign the dep. target value Watcher
pushTarget(this)
// Clear the current target
popTarget()
}
return value
}
// observer/dep.js
Dep.target = null
const targetStack = []
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
First we maintain both targetStack and dep.target globally. If computed does not exist before we initialize vue, our dep. target points to render Watcher. TargetStack also holds it. But to implement Watcher. Evaluate (), we use the watcher saved in this._computedWatchers. At this point, with the first method, we push to the targetStack, but after the evaluation, we popTarget the current Watcher. At this point, the current Watcher becomes a rendered Watcher.
To understand this, we execute watcher.depend().
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
Copy the code
This. Deps we save the calculation Watcher. It’s length 1, ok, so we’re doing the dependency collection again, but the current Watcher is the render Watcher. After performing the dependency collection, we can see that the subs array under the Dep instance in the current A contains two Watchers.
{
id: 3.subs: [
Watcher, / / calculate the Watcher
Watcher / / render Watcher]}Copy the code
After that we return watcher. Value, which is the evaluated value of watcher, and then vm._update, which renders vNode to the page. First render complete
Let’s think about some questions
- How is reactive caching implemented
Let’s look at an example
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta Name ="viewport" content="width=device-width, initial-scale=1.0"> <title>computed</title> <script SRC =".. /.. /dist/vue.js"></script> </head> <body> <div id="app"> <div>{{compA}}</div> <div>{{compA}}</div> </div> <script> const vm = new Vue({ el: '#app', data: { a: 1, }, computed: { compA() { return this.a + 1 } } }) </script> </body> </html>Copy the code
What difference does this[‘compA’] make the second time we put a compA on it?
First of all, let’s be clear that in computing Watcher, we defined two properties dirty=lazy=true. But we assign dirty to false during the first evaluation.
evaluate () {
this.value = this.get()
this.dirty = false
}
if (watcher.dirty) {
watcher.evaluate()
}
Copy the code
So when we execute the second time, watcher.dirty is false, so it won’t execute. That’s the benefit of closures. While watcher. Depend is executed, its repeated collection is related to the addDep method
addDep (dep: Dep) {
const id = dep.id
// * Check if the unique id already exists in the set in a single evaluation,
if (!this.newDepIds.has(id)) {
}
}
Copy the code
NewDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds = this.newDepIds This is the implementation of caching. As for this de-duplication problem, let’s wait for the analysis of responsive data later.
Update the data
They are reevaluated only when the associated reactive dependencies change
Let’s try to update the value of a and see what happens. In console, execute vm. A = 3. At this point we trigger the dep.notify() method of the set function in the Data interceptor.
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// Loop through all observer objects
subs[i].update()
}
}
Copy the code
As you can see from above, we execute Watcher’s update methods sequentially. Take a look at the implementation of the Update method
Update () {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {if (this.sync) {this.run()} QueueWatcher (this)}} queueWatcher(this)}Copy the code
The first Watcher is the computed Watcher, so its lazy is true. In this section of code, only this.dirty=true is executed. Okay, I’m going to keep this in suspense.
Then we look at the execution of the second render Watcher, and lazy is no longer true, we execute queueWatcher. In the flushSchedulerQueue method, a Watcher queue is first stored and executed in nextTick, i.e., in the microtask before the next event loop. In the flushSchedulerQueue method, watcher.run is executed and then evaluated again. Update the value, update the DOM
We won’t go into details until we analyze nextTick.
Let’s start with the above question
Why is this.dirty assigned to true?
To answer this question, let’s look at the most recent example of anonymous execution code
; (function anonymous () {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('div', [_v(_s(compA))]),
_v(' '),
_c('div', [_v(_s(compA))])
])
}
})
Copy the code
You can see that we executed this[‘compA’] twice, so the overall flow is obvious
This [‘compA’] dep.notify–>dep.update–>queueWatcher–>watcher.run–>this.getter. In the first calculation Watcher we assigned this.dirty = true. Then vm._update(vm._render) in the render function is executed. That is, execute the anonymous function above
This triggers the computedGetter, but our dirty is true, so for the first time we will reevaluate the calculated property.
Then the second this[‘compA’] comes in just like the initialization.
conclusion
This gives us a thorough explanation of the core implementation of computed, as well as the answer to the official website. It makes no difference if you add dependency execution.