preface

When people think of computed data in Vue, the first thing they think is that computed properties are cached, so how exactly is it cached? There are a lot of people who are still confused about what the cache is and when it expires.

This article, based on Vue 2.6.11, takes a closer look at what caching actually looks like.

Pay attention to

This article assumes that you already have a basic understanding of the Vue responsive principle. If you are not familiar with Watcher, Dep, and the concept of rendering Watcher, you can find some articles or tutorials on the basic responsive principle. If you want to see the simplified implementation, you can also see the article I wrote:

Learn Vue source code for Data, computed, and Watch by implementing a minimalist responsive system

Note that I have also written about the principle of computed tomography in this article, but computed in this article is based on Vue version 2.5, and the change from the current version 2.6 is very big, so it can only be used for reference.

The sample

In keeping with my convention, I’m going to use a very simple example.

<div id="app">
  <span @click="change">{{sum}}</span>
</div>
<script src=". / vue2.6. Js. ""></script>
<script>
  new Vue({
    el: "#app",
    data() {
      return {
        count: 1,
      }
    },
    methods: {
      change() {
        this.count = 2
      },
    },
    computed: {
      sum() {
        return this.count + 1}},})</script>
Copy the code

This example is very simple. The number 2 is displayed at the beginning of the page, and when you click the number, it becomes 3.

parsing

Review watcher’s process

To get down to business, when Vue first runs, it does some initialization for computed properties. First, let’s review watcher’s concepts, whose core concepts are GET evaluation and update.

  1. When evaluating, it first assigns itself, watcher itself, to the dep. target global variable.

  2. And then when you evaluate, you read the reactive property, and the DEP of the reactive property collects this Watcher as a dependency.

  3. The next time a reactive property is updated, it retrieves the watcher it collected from the DEP and triggers watcher.update() to update it.

The key is what the GET does and what updates the update triggers.

In the basic reactive view update process, the above concept of GET evaluation refers to the Vue component re-render function, and update, in fact, re-call the component render function to update the view.

The neat thing about Vue is that this process also works for computed updates.

Initialize the computed

For a preview, Vue also wraps every computed property in options with Watcher, and its GET function performs a user-defined evaluation function, while update is a more complex process, which I’ll explain in more detail.

First, when the component is initialized, it enters a function that initializes computed

if (opts.computed) { initComputed(vm, opts.computed); }
Copy the code

Go to initComputed and take a look

var watchers = vm._computedWatchers = Object.create(null);

// Define each computed attribute in turn
for (const key in computed) {
  const userDef = computed[key]
  watchers[key] = new Watcher(
      vm, / / instance
      getter, // The user passes in the evaluation function sum
      noop, // The callback function can be ignored for now
      { lazy: true } // Declare the lazy attribute to mark computed Watcher
  )

  // what happens when the user calls this.sum
  defineComputed(vm, key, userDef)
}
Copy the code

We first define an empty object to hold all the calculated property-related Watcher, which we will call the calculated Watcher later.

The loop then generates a calculated watcher for each computed property.

Its form retains key properties and, after simplification, looks like this:

{
    deps: [].dirty: true.getter: ƒ sum (),lazy: true.value: undefined
}
Copy the code

Its value is undefined, and its lazy value is true, which means that its value is lazy and not evaluated until it is actually read from the template.

This dirty property is actually the key to caching, so keep it in mind.

Let’s look at the crucial defineComputed, which determines what happens when the user reads the value of the this.sum calculated property, further simplifying and eliminating some logic that doesn’t affect the process.

Object.defineProperty(vm, 'sum', { 
    get() {
        // Get computed Watcher from the component instance just described
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // ✨ note! This is only reevaluated if it's dirty
          if (watcher.dirty) {
            // Get is evaluated here
            watcher.evaluate()
          }
          // ✨ is also a key point
          if (Dep.target) {
            watcher.depend()
          }
          // Returns the calculated value
          return watcher.value
        }
    }
})
Copy the code

This function needs to be looked at carefully. It does several things, and we’ll show it in the process of initialization:

First, the concept dirty refers to dirty data, indicating that the data needs to be evaluated again by calling the sum function passed in by the user. Leaving aside the logic at update time, {{sum}} must be true when it is first read from the template, so initialization goes through an evaluation.

evaluate () {
  // Call the get function to evaluate
  this.value = this.get()
  // Mark dirty as false
  this.dirty = false
}
Copy the code

This function is actually quite clear, it evaluates first and sets dirty to false.

Going back to the logic of object.defineProperty,

The next time there is no special case where sum is false, we can return the value watcher.value.

update

Now that the initialization process is over, you have a general idea of dirty and cache (if not, take a second look).

Let’s take a look at the update process. In the example of this article, how the update of count triggers the change of sum on the page.

Let’s go back to the evalute function, which evaluates sum when it reads dirty data.

evaluate () {
  // Call the get function to evaluate
  this.value = this.get()
  // Mark dirty as false
  this.dirty = false
}
Copy the code

Dep.target changed to render Watcher

If {{sum}} is read from the template, the dep. target should be the render watcher.

The global dep. target state is stored in a targetStack, which allows you to advance and reverse dep. target, as shown in the following function.

The dep. target is render watcher and the targetStack is [Render Watcher].

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } finally {
    popTarget()
  }
  return value
}
Copy the code

PushTarget = Dep. Target; pushTarget = Dep. Target;

After pushTarget(this),

Dep.target is changed to calculate watcher

The dep. target is calculated watcher, and the targetStack is [Render watcher, calculate Watcher].

Value = this.getter.call(vm, vm),

The getter function, as explained in the Watcher form in the previous chapter, is the sum function passed in by the user.

sum() {
    return this.count + 1
}
Copy the code

Here, at execution time, this.count is read, notice that it’s a responsive property, so somehow they start to make connections…

This is going to trigger get hijacking of count, just to simplify things

// In the closure, the deP defined for the key count is retained
const dep = new Dep()

// The closure also preserves the val set last time
let val

Object.defineProperty(vm, 'count', {
  get: function reactiveGetter () {
    const value = val
    // dep. target is used to calculate watcher
    if (Dep.target) {
      // Collect dependencies
      dep.depend()
    }
    return value
  },
})
Copy the code

So you can see that count is going to be counted and watcher is going to be counted

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

AddDep (this) is the addDep function of Watcher. This is the result of some internal de-weighting optimization.

// Watcher's addDep function
addDep (dep: Dep) {
  // A series of de-duplicates are done here to simplify things
  
  // the dep of count is stored in its own deps
  this.deps.push(dep)
  // Take watcher itself as a parameter
  // return to the addSub function of dep
  dep.addSub(this)}Copy the code

It’s back to deP.

class Dep {
  subs = []

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}
Copy the code

This preserves the dependency in the DEP to count watcher as count.

After going through such a collection process, some states at this point:

Watcher:

{
    deps: [dep of count],dirty: false.// the value is false
    value: 2.// 1 + 1 = 2Getter: ƒ sum (),lazy: true
}
Copy the code

The count of dep:

{
    subs: [sum calculation watcher]}Copy the code

As you can see, the Watcher that calculates the attributes and the DEP of the reactive values that it depends on preserve each other.

At this point, the evaluation is finished, and we are back to calculating watcher’s getter function:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } finally {
    // This is the end of the execution
    popTarget()
  }
  return value
}
Copy the code

PopTarget is executed, and watcher is calculated out of the stack.

Dep.target changed to render Watcher

The dep. target is render watcher and the targetStack is [Render Watcher].

Then the function completes and returns a value of 2, while the get access to the sum property is still in progress.

Object.defineProperty(vm, 'sum', { 
    get() {
          // At this point the function is executed
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
    }
})
Copy the code

The dep.target of course has a value, which is the render watcher, so the logic of watcher.depend() is very important.

// watcher.depend
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
Copy the code

Remember how we calculated watcher’s form? It holds the DEP of count in its DEPS.

That is, dep.depend() on count is called again

class Dep {
  subs = []
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}}Copy the code

The dep. target is already the render Watcher, so the count Dep will store the render Watcher in its subs.

The count of dep:

{
    subs: [sum calculated watcher, render watcher]}Copy the code

So that brings us to the point of this problem, how do we trigger view updates when count is updated?

Back to count’s reactive hijacking logic:

// In the closure, the deP defined for the key count is retained
const dep = new Dep()

// The closure also preserves the val set last time
let val

Object.defineProperty(vm, 'count', {
  set: function reactiveSetter (newVal) {
      val = newVal
      // Trigger notify of count's dep
      dep.notify()
    }
  })
})
Copy the code

Ok, this triggers the notify function of the deP of count that we’ve just carefully prepared, and it feels like we’re getting closer to success.

class Dep {
  subs = []
  
  notify () {
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Copy the code

The logic here is simple: call the update method of the watcher saved in subs

  1. callCalculate the watcherThe update of
  2. callRender the watcherThe update of

Let’s break it down.

Calculate the update to watcher

update () {
  if (this.lazy) {
    this.dirty = true}}Copy the code

WTF, that’s all… That’s right, just set the dirty property of watcher to true and wait for the next read.

Render watcher update

Call vm._update(vm._render()) and re-render the vNode based on the render function.

Sum = sum; sum = sum; sum = sum; sum = sum;

Object.defineProperty(vm, 'sum', { 
    get() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // ✨ set dirty to true in the previous step, so it will be reevaluated
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          // Returns the calculated value
          return watcher.value
        }
    }
})
Copy the code

Due to the reactive attribute update in the previous step, the dirty update of the calculated Watcher is triggered to true. So the sum function passed in by the user is called again to calculate the latest value, which is displayed on the page.

At this point, the entire process of calculating property updates is complete.

The cache takes effect

According to the summary above, dirty is reset to true only when the reactive value of the calculated attribute dependence is updated, so that the actual calculation will occur the next time it is read.

In this case, the optimization is obvious, assuming that the sum function is a user-defined and time-consuming operation.

<div id="app">
  <span @click="change">{{sum}}</span>
  <span @click="changeOther">{{other}}</span>
</div>
<script src=". / vue2.6. Js. ""></script>
<script>
  new Vue({
    el: "#app",
    data() {
      return {
        count: 1.other: 'Hello'}},methods: {
      change() {
        this.count = 2
      },
      changeOther() {
        this.other = 'ssh'}},computed: {
      // Very time consuming computing properties
      sum() {
        let i = 9999999999999999
        while(i > 0) {
            i--
        }
        return this.count + 1}},})</script>
Copy the code

In this example, the value of other has nothing to do with the calculated property. If the value of other triggers an update, the view will be rerendered and sum will be read. If the calculated property is not cached, a performance-costly and unnecessary calculation will occur every time.

So, sum is recalculated only if count changes, which is a neat optimization.

conclusion

The path for calculating property updates in version 2.6 looks like this:

  1. Reactive valuecountupdate
  2. At the same time informcomputed watcherRender the watcherupdate
  3. computed watcherSet dirty to true
  4. View rendering reads values for computed, due to dirtycomputed watcherReevaluate.

With this article, you have a good understanding of what caching of computed attributes really is and under what circumstances it works.

For cached and uncached cases, the flow looks like this:

Don’t cache:

  1. countChange, notice firstCalculate the watcherUpdate, setdirty = true
  2. Notice toRender the watcherUpdate when the view is re-renderedCalculate the watcherMedium read valuedirtyIf true, re-execute the function evaluation passed in by the user.

Cache:

  1. otherChange, direct noticeRender the watcherThe update.
  2. Go when the view is re-renderedCalculate the watcherMedium read valuedirtyIf false, use the cached value directlywatcher.valueDoes not perform the function evaluation passed in by the user.

Looking forward to

In fact, this method of calculating attribute cache through dirty flag bits is the same as Vue3 implementation principle. This may also indicate that, given the variety of needs and community feedback, Utah now considers this approach to be a relatively optimal solution for computed caching.

If you are interested in a computed implementation of Vue3, you can also read this article. The principle is very much the same. It’s just a slightly different way of collecting.

In-depth analysis: How does Vue3 implement powerful computed smartly

❤️ thank you

1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.

2. Follow the public account “front-end from advanced to hospital” to add my friends, I pull you into the “front-end advanced communication group”, we communicate and progress together.