Explain the cache implementation principle of vue computed in detail

  • 2021-11-01 23:40:58
  • OfStack

Directory initialization computed
Dependency collection
Distribute updates
Summary 1

This article focuses on the following example to explain the process of computed initialization and update under 1, to see how computed attributes are cached and how dependencies are collected.


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

Initialize computed

When vue is initialized, the init method is executed first, and the initState in vue will initialize the calculation attribute


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

Here's the code for initComputed


var watchers = vm._computedWatchers = Object.create(null); 
//  In turn, each  computed  Attribute definition 1 Calculations watcher
for (const key in computed) {
  const userDef = computed[key]
  watchers[key] = new Watcher(
      vm, //  Instances 
      getter, //  Evaluation function passed in by user  sum
      noop, //  Callback function   You can ignore it first 
      { lazy: true } //  Declaration  lazy  Attribute   Mark  computed watcher
  )
  //  The user is calling  this.sum  What will happen when you are 
  defineComputed(vm, key, userDef)
}

The initial state of the calculation watcher corresponding to each calculation attribute is as follows:


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

It can be seen that its value is undefined at first, and lazy is true, which shows that its value is calculated lazily, and it will only be calculated after reading its value in the template.

This dirty attribute is actually the key to caching, so remember it first.

Next, let's look at the key defineComputed, which determines what happens after the user reads the value of this. sum. Continue to simplify and eliminate some logic that does not affect the process.


Object.defineProperty(target, key, { 
    get() {
        //  Get it from the component instance just mentioned  computed watcher
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          //  Only dirty It will be re-evaluated after it is completed 
          if (watcher.dirty) {
            //  The value will be evaluated here, and the call will be made get That sets the Dep.target
            watcher.evaluate()
          }
          //  This is also the key   I'll talk about it later 
          if (Dep.target) {
            watcher.depend()
          }
          //  Finally, the calculated value is returned 
          return watcher.value
        }
    }
})

This function needs to be taken a closer look. It does several things. Let's explain it with the initialization process:

First of all, the concept of dirty represents dirty data, indicating that this data needs to be evaluated by calling the sum function passed in by the user again. Regardless of the update logic, the first time {{sum}} is read in the template, it must be true, so the initialization will undergo one evaluation.


evaluate () {
  //  Call  get  Function evaluation 
  this.value = this.get()
  //  Put  dirty  Marked as  false
  this.dirty = false
}

This function is actually very clear. It evaluates first, and then sets dirty to false. Looking back at the logic of Object. defineProperty just now, the next time we read sum without special circumstances, we find that dirty is false. Is it enough to directly return the value of watcher. value? This is actually the concept of calculating attribute cache?

Dependency collection

After initialization, render is finally called for rendering, and the render function will be used as getter of watcher, and watcher at this time will be rendered watcher.


updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
//  Create 1 Render watcher , rendering watcher When initialized, its get() Method, namely render Function, dependency collection is performed 
new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */)

Look at the get method in watcher under 1


get () {
    //  Will the current watcher Put it on the top of the stack and set it to Dep.target
    pushTarget(this)
    let value
    const vm = this.vm
    //  Calling a user-defined function accesses the this.count To access its getter Method, which will be discussed below 
    value = this.getter.call(vm, vm)
    //  After the evaluation is completed, the current watcher Out of stack 
    popTarget()
    this.cleanupDeps()
    return value
 }

When the getter of rendering watcher is executed (render function), this. sum will be accessed, which will trigger getter of the calculation attribute, that is, the method defined at initComputed will get the calculation watcher bound to sum. Because dirty is true at initialization, its evaluate method will be called, and finally its get () method will be called, and the calculation watcher will be put on the top of the stack. At this time, Dep. target is also the calculation watcher.

Then call its get method, which will access this. count, trigger getter of count attribute (as follows), and collect watcher stored by current Dep. target into dep corresponding to count attribute. When the evaluation is finished, call popTarget () to take the watcher out of the stack, the last rendering watcher is at the top of the stack, and Dep. target is rendered watcher again.


//  In the closure, the  count  This  key  Defined by  dep
const dep = new Dep()
 
//  Closure will also be preserved in the 1 Times  set  Function set by the  val
let val
 
Object.defineProperty(obj, key, {
  get: function reactiveGetter () {
    const value = val
    // Dep.target  This is the calculation watcher
    if (Dep.target) {
      //  Collect dependencies 
      dep.depend()
    }
    return value
  },
})

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

if (opts.computed) {initComputed(vm, opts.computed);}
0

if (opts.computed) {initComputed(vm, opts.computed);}
1

With these two pieces of code, the computed watcher is collected by the attribute-bound dep. watcher depends on dep, and dep also depends on watcher. This interdependent data structure between them makes it easy to know which dep an watcher depends on and which watcher an dep depends on.

Then execute watcher. depend ()


if (opts.computed) {initComputed(vm, opts.computed);}
2

Remember just calculating the shape of watcher? Its deps preserves dep of count. That is, dep. depend () on count is called again


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

This time, Dep. target is already rendering watcher, so dep of count will store rendering watcher in its own subs.

Finally, the dependencies of count are collected, and its dep is:


if (opts.computed) {initComputed(vm, opts.computed);}
4

Distribute updates

Then came to the point of this question, at this time count updated, how to trigger the view update?

Going back to the responsive hijacking logic of count:


if (opts.computed) {initComputed(vm, opts.computed);}
5

OK, this triggers the notify function of dep of count that we have just carefully prepared.


if (opts.computed) {initComputed(vm, opts.computed);}
6

The logic here is very simple. The watcher stored in subs calls their update methods in turn, that is,

Call update that evaluates watcher Call update for rendering watcher

Calculate update of watcher


if (opts.computed) {initComputed(vm, opts.computed);}
7

Simply set the dirty attribute of watcher to true, and wait quietly for the next read (when render function is executed again, the sum attribute will be accessed again, and when dirty is true, the evaluation will be carried out again).

Render update of watcher

In fact, this function is called vm._update (vm._render ()), and the view is rendered again according to vnode generated by render function.
In the process of render, 1 must access the value of su, so it goes back to get defined by sum:


Object.defineProperty(target, key, { 
    get() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          //  Upper 1 Step  dirty  Has been set to  true,  So it will be re-evaluated 
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          //  Finally, the calculated value is returned 
          return watcher.value
        }
    }
})

The dirty from which watcher is calculated is triggered to be updated to true due to the responsive property update in the previous step 1. Therefore, the sum function passed in by the user will be called again to calculate the latest value, and the latest value will naturally be displayed on the page.

At this point, the whole process of calculating attribute updates is over.

Summary 1

Initialize data and computed, proxy their set and get methods, respectively, and generate only one dep instance for all attributes in data. Generate only 1watcher for sum in computed and save it in vm._computedWatchers The sum property is accessed when the render function is executed, so that the getter method defined when initComputed is executed points Dep. target to watcher of sum and calls the property concrete method sum. Access to this. count in the sum method calls the get method of the this. count proxy, adding dep of this. count to watcher of sum, and adding this watcher to subs in the dep. Setting vm. count = 2, calling the set method of the count proxy triggers the notify method of dep because it is the computed property, only setting dirty in watcher to true. In the last step, vm. sum, when accessing its get method, we know that watcher. dirty of sum is true, and call its watcher. evaluate () method to get a new value.

The above is the detailed explanation of vue computed cache implementation principle details, more information about vue computed cache implementation please pay attention to other related articles on this site!


Related articles: