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

  1. Initialize thecomputed

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

  1. To create awatchers = vm._computedWatchers = null
  2. getcomputedThe method inside is assigned togetterAnd thennew WatcherAnd assign a value towatchers[key]
  3. performdefineComputedMethod, this is going to tell youcomputedthekeyIs here or notvmIn, 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.

  1. whenuserDefIs a method used in non-server rendering environmentscreateComputedGetter(userDef)And,setIs an empty function
  2. whenuserDefIs an objectcreateComputedGetter(userDef.get)And thesetThe 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
  1. fromvm.$optionsTo deriverender.
  2. performrenderMethods. Pass in the current instance, andvm.$createElement.vm.$createElementisinitRenderTime 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

  1. 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.