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

Detailed explanation of several methods of JS array dimensionality reduction

Dimensionality reduction of two-dimensional array...

Common HTML tag writing errors

We better start paying attention, because HTML Po...

Detailed explanation of concat related functions in MySQL

1. concat() function Function: Concatenate multip...

Vue project realizes paging effect

The paging effect is implemented in the vue proje...

Detailed explanation of Nginx status monitoring and log analysis

1. Nginx status monitoring Nginx provides a built...

How to use CSS to pull down a small image to view a large image and information

Today I will talk about a CSS special effect of h...

Use of MySQL stress testing tool Mysqlslap

1. MySQL's own stress testing tool Mysqlslap ...

Linux command line operation Baidu cloud upload and download files

Table of contents 0. Background 1. Installation 2...

Summary of the use of MySQL date and time functions

This article is based on MySQL 8.0 This article i...

Detailed explanation of Nginx rewrite jump application scenarios

Application scenario 1: Domain name-based redirec...

A complete guide to some uncommon but useful CSS attribute operations

1. Custom text selection ::selection { background...

Web data storage: Cookie, UserData, SessionStorage, WebSqlDatabase

Cookie It is a standard way to save the state of ...