preface

Computed in Vue is a property that is often used in daily development, and it is also a topic that is often asked in interviews. You can find a question like this in almost any interview question collection related to Vue: What is the difference between methods and computed? Without hesitation, you might say, “Methods don’t get cached, computed does.” Sure, this cache is a major feature, but what does this cache mean? How is caching implemented? In which case will it not be cached? When will the cache be reevaluated? What are the benefits of caching? In addition to caching, we can also ask: How do WE use setters in evaluating properties? Can calculated attributes depend on other calculated attributes? What is the internal mechanism? Many of you may not be familiar with these questions, but this article will give you an in-depth understanding of this computational property. Don’t be afraid to ask any interviewer.

The Vue source version used in this article is 2.6.11

DEMO

Let’s start with a simple example that will be analyzed in this article:

<div id="app">
  <div @click="add">DoubleCount: {{doubleCount}}</div>
</div>
<script>
  new Vue({
    el: '#app'.name: 'root'.data() {
      return {
        count: 1}},computed: {
      doubleCount() {
        return this.count * 2}},methods: {
      add() {
        this.count += 1}}})</script>
Copy the code

Here we use a doubleCount calculation property, which is twice the value of count. Each click increases the value of count by one, and doubleCount changes accordingly.

The principle of analysis

First of all, you have to understand the principle of Vue’s responsive system. If you don’t understand it, you can go to the Internet to search for articles in this area.

The Vue source code posted in this article is not the original source code, in order to facilitate the analysis, the original source code has been simplified, in addition to unimportant logic and boundary case processing.

Look directly at the source code

Initialization process

The initState function is executed when the component is initialized:

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 */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

After data is initialized, initComputed is used to initialize the properties of props, data, and methods. This is why we can access the properties of props, data, and methods directly in the computed properties, because the initialization occurs after these three properties. Here’s the logic for initComputed:

// VM is the component instance, and computed is the object we defined in options.
function initComputed(vm: Component, computed: Object) {
  // Create a watchers object, which is empty
  const watchers = (vm._computedWatchers = Object.create(null))
  for (const key in computed) {
    // Get the definition of the calculated property. For our example, userDef is the doubleCount function
    const userDef = computed[key]
    // Since doubleCount is a function, the getter here is still doubleCount
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // Create a Watcher and save it to Watchers
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )
    / / to speak
    defineComputed(vm, key, userDef)
  }
}
Copy the code

This function is computed by traversing defined, creating a Watcher for each calculated attribute and saving it in Watchers, which is on the _computedWatchers attribute of the VM, A computedWatcherOptions is passed in to create a watcher, which is an object with only lazy attributes:

const computedWatcherOptions = { lazy: true }
Copy the code

Here’s a quick look at Watcher:

class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    // Options are computedWatcherOptions, so lazy is true
    this.lazy = !! options.lazy// To control the cache, more on that later
    this.dirty = this.lazy // true
    this.deps = [] // Collected Dep
    // The evaluation method, for our example, is the doubleCount function
    this.getter = expOrFn
    // Initialize value. Lazy is true, so nothing is executed
    this.value = this.lazy ? undefined : this.get()
  }
}
Copy the code

The key points here are the lazy attribute and the dirty attribute. Lazy evaluates lazily and does not evaluate when the value is initialized because lazy is true. We’ll talk about dirty later, but then we’ll look at computed initialization.

DefineComputed (VM, key, userDef) is also executed after watcher is created:

export function defineComputed(
  target: any, // vm
  key: string, // Computed key: 'doubleCount'
  userDef: Object | Function // Calculate the value of the attribute, the doubleCount function
) {
  // Set the getter and setter using defineProperty
  Object.defineProperty(target, key, {
    enumerable: true.configurable: true.get: function computedGetter() {
      // Get the Watcher created in initComputed
      const watcher = this._computedWatchers && this._computedWatchers[key]
      if (watcher) {
        / /!!!!! Evaluate is executed only when dirty is true
        if (watcher.dirty) {
          // evaluate evaluates watcher and sets dirty to false
          watcher.evaluate()
        }
        / / to speak
        if (Dep.target) {
          watcher.depend()
        }
        // Return watcher
        return watcher.value
      }
    }
  })
}
Copy the code

DefineComputed sets up the proxy primarily through defineProperty, and this get function is executed when the calculated property is accessed through the instance.

Implementation of caching

Imagine a scene being rendered for the first time, count is 1, and accessing the doubleCount property in the template executes the get function defined in defineComputed, which first gets the watcher defined in initComputed, Evaluate (); evaluate(); evaluate()

class Watcher {
  constructor() {
    // ...
  }
  evaluate() {
    this.value = this.get() // Get evaluates watcher, more on that later
    this.dirty = false // Reset dirty to false}}Copy the code

Here the get function executes, which executes watcher’s getter, or in our case, doubleCount: Return this.count * 2, assigns the result to value, sets dirty to false, and then watcher has a value. Return watcher. Value returns the value of watcher and renders doubleCount in the template: In mounted console.log(this.doublecount), we will return to defineComputed get. So instead of executing watcher.evaluate(), doubleCount will simply return watcher.value, which is 2, and cache it.

If we change count from 1 to 2, then the next time we visit doubleCount, we should get 4, so when was this cache updated, and how was it updated? Don’t worry. Let’s move on.

The cache update

First, let’s review the flow of Vue’s responsive system. Vue’s responsive system is mainly implemented through Watcher, Dep and Object.defineProperty. When initializing data, Set the getter and setter for the property with Object.defineProperty to make the property responsive, and then create a Watcher when performing some operation (render operation, calculate property, customize Watcher, etc.). The watcher points a global variable, dep. target, to itself before performing the evaluation. Then, if a reactive property is accessed during the evaluation, the current dep. target (watcher) is added to the property’s Dep. Then, the next time the reactive property is updated, The collected watcher is retrieved from the DEP, and the update operation is performed by executing watcher.update.

Summary of the relatively brief, if you do not understand the words, it is suggested to go online to search the article in this respect

Watcher.evaluate () = this.value = this.get(); watcher.evaluate(); Let’s take a look at this.get

class Watcher {
  constructor() {
    // ...
  }
  get() {
    // targetStack saves the current Watcher stack
    // Other watcher may be created during watcher evaluation
    targetStack.push(this)
    // Point dep. target to itself
    Dep.target = this

    let value
    const vm = this.vm
    // Execute the getter function, which for our example is the doubleCount function
    value = this.getter.call(vm, vm)

    // The current watcher is off the stack
    targetStack.pop()
    // Revert to the previous watcher
    Dep.target = targetStack[targetStack.length - 1]

    return value
  }
}
Copy the code

Dep.target is set and the getter is executed. DoubleCount accesses the count property, so it is executed in the getter of count:

function defineReactive(obk, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter() {
      const value = val
      // Dep.target is the watcher that calculates the property
      if (Dep.target) {
        // Execute depend to collect dependencies
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      / /... Speak later}})}Copy the code

This get basically does dep.depend() collecting dependencies:

class Dep {
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)}}}Copy the code

Here we execute Watcher’s addDep, passing itself in as a parameter:

class Watcher {
  addDep(dep) {
    dep.addSub(this)
    this.deps.push(dep)
  }
}
Copy the code

To add deP to Watcher’s own DEps, we call dep.addsub (this) with the argument itself, which returns to deP:

class Dep {
  addSub(sub) {
    this.subs.push(sub)
  }
}
Copy the code

Subs = dep = watcher; subs = dep = watcher; subs = dep = watcher Deps will be the dep of [count], and both have references to each other. We can draw the conclusion that calling a deP’s Depend method adds dep. target to its subs (which we’ll use later). This is what we do when we initialize the value, and when we set count to 2, we go to setter logic for count:

function defineReactive(obk, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter() {
      / /...
    },
    set: function reactiveSetter(newVal) {
      val = newVal
      const subs = dep.subs.slice()
      // Iterate over subs, execute update
      for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  })
}
Copy the code

Here we retrieve the previously saved watcher, iterate through it and execute watcher.update:

class Watcher {
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}}Copy the code

This is the key logic, because lazy is true to evaluate the attribute, so this.dirty = true is executed, and that’s it. If the logic ends here, where does the calculated property get reevaluated? Where does the view get rerendered? If this logic is followed, the calculated properties are not updated at all, and the view is not re-rendered, what is the problem?

How views are updated

One thing we’ve been missing is render Watcher, and when you render watcher, you do render Watcher first, and then you do render function in render Watcher, and then you call doubleCount in the render function, Watcher.get () : evaluate() : evaluate() : evaluate() : evaluate();

class Watcher {
  constructor() {
    // ...
  }
  get() {
    // Access to doubleCount takes place in render Watcher
    // So before executing the following line of code, inside the targetStack is: [render watcher]
    targetStack.push(this) // After executing this code, the targetStack is: [render watcher, calculate Watcher]
    Dep.target = this

    let value
    const vm = this.vm
    // Collect dependencies as before
    // dep.subs of count will be [calculate attribute watcher], calculate attribute watcher deps will be [count dep]
    value = this.getter.call(vm, vm)

    // The current watcher is off the stack
    targetStack.pop() // After executing this code, the targetStack will be: [render watcher]
    // Revert to the previous watcher
    Dep.target = targetStack[targetStack.length - 1] // Dep.target is: render watcher

    return value
  }
}
Copy the code

Return to the getter for defineComputed after executing this get:

get: function computedGetter() {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      // Evaluate watcher
      watcher.evaluate()
    }
    // Dep.target is the render watcher, so there is a value here
    if (Dep.target) {
      // Perform watcher's collection dependency operation
      watcher.depend()
    }
    return watcher.value
  }
}
Copy the code

Since dep.target has a value, it executes watcher.depend().

class Watcher {
  constructor() {
    // ...
  }
  depend() {
    // 上文已经分析过,计算watcher的deps是:[count的dep]
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
Copy the code

Here we iterate through dePS and execute deP’s Depend method, remember that method and that conclusion? Dep. Target = render watcher; deP. Target = render watcher; deP. After executing the Depend, the dep.subs of count is [calculate property watcher, render watcher].

So you might see by now, when you update a reactive property, in the setter for count, you iterate through the SUBs of the DEP and perform the update method, and in that subs you don’t just have the watcher that calculates the property, you also have the render watcher, Let’s look at the update method again:

class Watcher {
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}}Copy the code

Update to evaluate attributes, set dirty to true, then render watcher update, render Watcher lazy and sync to false, so queueWatcher(this), The queueWatcher method is a queueWatcher method that you don’t need to worry about. It actually ends up executing the render function in Watcher, which in turn calls doubleCount:

get: function computedGetter() {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    // At this point dirty is already true, meaning it needs to be updated
    if (watcher.dirty) {
      // To evaluate watcher, execute doubleCount
      // After execution, watcher.value will change from 2 to 4
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value / / return 4}}Copy the code

Since we set dirty to true in the update phase, we execute watcher.evaluate(), and doubleCount is updated and renders 4 on the page. If we change count again, The logic above is repeated.

If you don’t use doubleCount in the template, just listen for the calculated properties through Watch, which is similar logic, but change the render watcher to the user watcher. You can also break the process by yourself.

AAA calculates watcher,AA calculates watcher,A calculates watcher, render watcher]

conclusion

The following two points can be concluded from this paper:

  1. The lazy value of the calculated property watcher is true when modifying the reactive propertywatcher.update, instead of evaluating watcher, willwatcher.dirtySet it to true, and watcher will be evaluated only when dirty is found to be true the next time the calculated property is accessed.
  2. If the dependency of the evaluated property does not change, it will not be reevaluated no matter how many times we access it, and will be directly evaluated fromwatcher.valueReturn the value we need.

Many articles on Vue performance tuning refer to placing computation-intensive or frequently performed operations in calculation properties. It makes use of the characteristics of the compute attribute cache to reduce the meaningless calculation.

In addition to the content of this article, calculating the property also supports custom setters, and passing in other options, but it is relatively easy, you can read the source code analysis, if you thoroughly understand the content of this article, then whether in the interview or daily development, I believe you will be able to handle it easily.