Detailed explanation of the cache implementation principle of Vue computed

Detailed explanation of the cache implementation principle of Vue computed

This article focuses on the following example to explain the process of computed initialization and update, to see how the calculated properties are cached and how the 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 inside will initialize the calculated properties

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

Below is the code for initComputed

var watchers = vm._computedWatchers = Object.create(null); 
// Define a computed watcher for each computed property in turn
for (const key in computed) {
  const userDef = computed[key]
  watchers[key] = new Watcher(
      vm, // instance getter, // user passed in evaluation function sum
      noop, // callback function can be ignored first { lazy: true } // declare lazy attribute to mark computed watcher
  )
  // What happens when the user calls this.sum defineComputed(vm, key, userDef)
}

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

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

You can see that its value is undefined at the beginning and lazy is true, which means that its value is calculated lazily and will not be calculated until its value is actually read in the template.

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

Next, let’s look at the more critical defineComputed, which determines what happens after the user reads the value of the computed property this.sum. We will continue to simplify and exclude some logic that does not affect the process.

Object.defineProperty(target, key, { 
    get() {
        // Get the computed watcher from the component instance just mentioned
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // Only when dirty will it be re-evaluated if (watcher.dirty) {
            // This will evaluate, call get, and set Dep.target
            watcher.evaluate()
          }
          // This is also a key point and I will explain it in detail later if (Dep.target) {
            watcher.depend()
          }
          //Finally return the calculated value return watcher.value
        }
    }
})

This function needs 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, which means that the data needs to be evaluated by re-calling the sum function passed in by the user. Let’s ignore the update logic for now. The first time {{sum}} is read in the template, it must be true, so the initialization will go through an evaluation.

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

This function is actually very clear, it first evaluates and then sets dirty to false. Let’s look back at the logic of Object.defineProperty. Next time when sum is read without special circumstances, if dirty is false, we can just return the value of watcher.value. This is actually the concept of computed property caching.

Dependency Collection

After initialization is completed, render will be called for rendering, and the render function will serve as the getter of the watcher. At this time, the watcher is the rendering watcher.

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
// Create a rendering watcher. When the rendering watcher is initialized, its get() method, that is, the render function, will be called to collect dependencies new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */)

Take a look at the get method in watcher

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

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

Then calling its get method will access this.count, triggering the getter of the count attribute (as shown below), and collecting the watcher stored in the current Dep.target into the dep corresponding to the count attribute. At this point, the evaluation is finished and popTarget() is called to pop the watcher out of the stack. At this point, the previous rendering watcher is at the top of the stack, and Dep.target becomes the rendering watcher again.

// In the closure, the dep defined for the key count will be retained
const dep = new Dep()
 
// The closure will also retain the val set by the last set function
let val
 
Object.defineProperty(obj, key, {
  get: function reactiveGetter () {
    const value = val
    // Dep.target is now calculating the watcher
    if (Dep.target) {
      // Collect dependencies dep.depend()
    }
    return value
  },
})
// dep.depend()
depend() {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
// watcher's addDep function addDep (dep: Dep) {
  // A series of deduplication operations are performed here to simplify // Here, the dep of count is also stored in its own deps this.deps.push(dep)
  // With the watcher itself as a parameter // Return to dep's addSub function dep.addSub(this)
}
class Dep {
  subs = []
 
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}

Through these two pieces of code, the calculated watcher is collected by the attribute bound dep. Watcher depends on dep, and dep also depends on watcher. This interdependent data structure can easily know which deps a watcher depends on and which watchers a dep depends on.

Then execute watcher.depend()

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

Remember the calculation of the watcher form just now? Its deps stores the dep of count. That is, dep.depend() on count will be called again

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

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

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

{
    subs: [sum calculation watcher, rendering watcher]
}

Distribute Updates

Now we come to the key point of this question. When the count is updated, how to trigger the view update?

Let’s go back to the responsive hijacking logic of count:

// In the closure, the dep defined for the key count will be retained
const dep = new Dep()
 
// The closure will also retain the val set by the last set function
let val
 
Object.defineProperty(obj, key, {
  set: function reactiveSetter (newVal) {
      val = newVal
      // Trigger the notify of count's dep
      dep.notify()
    }
  })
})

Well, here the notify function of count's dep that we just carefully prepared is triggered.

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

The logic here is very simple. Call the update method of the watchers saved in subs in turn, that is,

  1. Call to calculate the watcher's update
  2. Calling the render watcher's update

Calculating watcher updates

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

Just set the dirty property of the calculation watcher to true and wait quietly for the next read (when the render function is executed again, the sum property will be accessed again, and dirty will be true at this time, so it will be evaluated again).

Rendering watcher updates

Here we actually call the vm._update(vm._render()) function to re-render the view according to the vnode generated by the render function.
In the rendering process, the value of su will be accessed, so we return to the get defined by sum:

Object.defineProperty(target, key, { 
    get() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // In the previous step, dirty was already set to true, so it will be re-evaluated if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          //Finally return the calculated value return watcher.value
        }
    }
})

Due to the responsive property update in the previous step, the dirty update of the calculated watcher is triggered to true. 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.

So far, the entire process of calculating attribute updates has ended.

To sum up

  1. Initialize data and computed, proxy their set and get methods respectively, and generate unique dep instances for all properties in data.
  2. Generate a unique watcher for the sum in computed and save it in vm._computedWatchers
  3. When the render function is executed, the sum attribute is accessed, thereby executing the getter method defined in initComputed, pointing Dep.target to the watcher of sum, and calling the specific method sum of the attribute.
  4. Accessing this.count in the sum method will call the get method of the this.count proxy, add the dep of this.count to the watcher of sum, and add the subs in the dep to this watcher.
  5. Set vm.count = 2, call the set method of the count agent to trigger the notify method of dep. Because it is a computed property, it only sets dirty in the watcher to true.
  6. In the last step, when vm.sum accesses its get method, it is learned that watcher.dirty of sum is true, and its watcher.evaluate() method is called to obtain the new value.

The above is a detailed explanation of the cache implementation principle of vue computed. For more information about the cache implementation of vue computed, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Vue——Solving the error Computed property "****" was assigned to but it has no setter.
  • What are the differences between computed and watch in Vue
  • Detailed explanation of watch and computed in Vue
  • The difference and usage of watch and computed in Vue
  • Difference between computed and watch in Vue
  • The difference and usage of watch, computed and updated in Vue
  • A brief understanding of the difference between Vue computed properties and watch
  • Vue computed property code example
  • A brief discussion on Vue's data, computed, and watch source codes
  • Why can watch and computed in Vue monitor data changes and their differences
  • The difference and usage of filter and computed in Vue

<<:  Example of how to upload a Docker image to a private repository

>>:  Docker private repository management and deletion of images in local repositories

Recommend

How to manage multiple projects on CentOS SVN server

One demand Generally speaking, a company has mult...

Apache ab concurrent load stress test implementation method

ab command principle Apache's ab command simu...

Issues and precautions about setting maxPostSize for Tomcat

1. Why set maxPostSize? The tomcat container has ...

How to distinguish MySQL's innodb_flush_log_at_trx_commit and sync_binlog

The two parameters innodb_flush_log_at_trx_commit...

Professional and non-professional web design

First of all, the formation of web page style main...

MySQL complete collapse query regular matching detailed explanation

Overview In the previous chapter, we learned abou...

Native JS to achieve blinds special effects

This article shares a blinds special effect imple...

Ubuntu 18.04 obtains root permissions and logs in as root user

Written in advance: In the following steps, you n...

JS implements the sample code of decimal conversion to hexadecimal

Preface When we write code, we occasionally encou...

Detailed explanation of JavaScript primitive data type Symbol

Table of contents Introduction Description Naming...

...

How to delete an image in Docker

The command to delete images in docker is docker ...

Vue+Echart bar chart realizes epidemic data statistics

Table of contents 1. First install echarts in the...