preface

Meaning and Usage

NextTick’s official explanation:

A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

The modification of the data in Vue will result in a change in the corresponding response of the interface. However, with the nextTick method, we can obtain the changed DOM in the callback function passed in to nextTick. This may sound a bit fantastic, but let’s directly use nextTick to experience the effect. For example, we have the following code:

<template>
   <div>
      <button @click='update'>Update the data</button>
      <span id='content'>{{message}}</span>
   </div>
</template>
<script>
  export default{
      data: {message:'hello world'
      },
      methods: {update(){
              this.message='Hello World'
              console.log(document.getElementById('content').textContent);
              this.$nextTick(() = >{
                  console.log(document.getElementById('content').textContent); })}}}</script>
Copy the code

The first output is Hello World, and the second output is an updated Hello World.

hello world Hello World

That is, our update to Message in the first line of the update method is not immediately synchronized to the SPAN, but rather calls back the function we passed in nextTick after updating the SPAN.

// Modify the data
vm.data = 'Hello'
//-- > DOM has not been updated yet

Vue.nextTick(function () {
  //-- > DOM updated
})
Copy the code

The update of the DATA in the Vue does not trigger the update of the DOM element synchronously, meaning that the DOM update is performed asynchronously and the function we passed in nextTick is called after the update. So why does Vue need nextTick? How does nextTick work?

explore

Let’s take a curious look at the implementation of the nextTick function to deepen our understanding of Vue’s underlying principles. To understand the design intention and implementation principle of nextTick, we need two pre-knowledge understandings:

  1. Vue responsive Principle (Understanding design intent)
  2. Browser event loop mechanism (Understand how it works)

Therefore, this article will briefly explain the above two parts first, and finally introduce the realization principle of nextTick in detail.

Response principle

This part mainly introduces the reactive implementation principle of Vue, you can skip it if you already understand it. The core of Vue response principle is data hijacking and dependency collection, mainly using Object.defineProperty() to intercept data access operations, we call this implementation data proxy; At the same time, we can obtain the dependence on data by intercepting the data GET method, and collect all the dependencies into a collection.

 Object.defineProperty(data, key, {
    enumerable: true.configurable: true.// Intercept get. This method intercepts when we access data.key
    get: function reactiveGetter () {
        // We collect dependencies here
        return data[key];
    },
    // Intercepts set when we assign a value to data.key
    set: function reactiveSetter (newVal) {
        // Notifies dependencies of UI changes when data changes}})Copy the code

To better understand how nextTick works, we need to implement a simplified version of Vue.

Vue class

First we implement a Vue class that creates a Vue object. Its constructor receives an options parameter that initializes the Vue.

class Vue{
    constructor(options){
       this.$el=options.el;
       this._data=options.data;
       this.$data=this._data;
       // Perform reactive processing on data
       new Observe(this._data); }}// Create a Vue object
new Vue({
    el:'#app'.data: {message:'hello world'}})Copy the code

In the above code we first create a class for Vue. The constructor is roughly the same as what we normally use with Vue. We only deal with the parameters EL and data for ease of understanding. Observe that the last line of the constructor creates an Observe object and passes data as an argument. Observe is the class that responds to data. Let’s look ata simple implementation of Observe.

Observe the class

The Observe class listens for data using the Object.defineProperty() method.

class Observe{
    constructor(data){
       // If the data passed is object
       if(typeof data=='object') {this.walk(data); }}// This method iterates through the attributes of the object and responds to them in turn
    walk(obj){
        // Get all attributes
        const keys=Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            // Listen on all attributes (data hijacking)
            this.defineReactive(obj, keys[i])
        }
    }
    defineReactive(obj,key){
        if(typeof obj[key]=='object') {// If the property is an object, then the walk method is recursively called
            this.walk(obj[key]);
        }
        const dep=new Dep();// The Dep class is used to collect dependencies
        const val=obj[key];
        Object.defineProperty(obj, key, {
            enumerable: true.configurable: true.// The get agent adds the dep. target, or Watcher object, to the dependency collection
            get() {
              // Dep.target is assigned when the Watcher object is created
              if (Dep.target) {
                dep.addSubs(Dep.target);
              }
              return val;
            },
            set(newVal) {
                val=newVal;
                // Dependent change response
                dep.notify(newVal)
            } 
          })
    }
}
Copy the code

In the above code we use the Dep class, where the dependencies we collect in the GET method of the hijacked data are stored.

Dep class

The following code is an implementation of the Dep class. It has an array of subs to store dependencies. The dependency in this case is the Watcher that we will define later.

class Dep{
   static target=null
   constructor(){
       this.subs=[];
   }
   addSubs(watcher){
       this.subs.push(watcher)
   }
   notify(newVal){
       for(let i=0; i<this.subs.length; i++){this.subs[i].update(newVal); }}}Copy the code

Watcher class

The observer class, which does all the work of observing changes in data, invokes the get method of the corresponding property in data to trigger the dependency collection, and performs the corresponding update after the data changes.

let uid=0
class Watcher{
    // VM is a Vue object, key is the property to observe, cb is the operation to observe data changes, usually referring to DOM changes
    constructor(vm,key,cb){
       this.vm=vm;
       this.uid=uid++;
       this.cb=cb;
       // Assign itself to the dep.taget static variable before invoking get to trigger a dependency collection
       Dep.target=this;
       // Trigger the proxy get method on the object, perform get add dependency
       this.value=vm.$data[key];
       // Empty as soon as used
       Dep.target=null;
    }
    // Update function that is executed when set triggers notify of Dep. Dom changes are executed in response to data changes
    update(newValue){
        // Only when the value changes
        if(this.value! ==newValue){this.value=newValue;
            this.run(); }}// Perform operations such as DOM updates
    run(){
        this.cb(this.value); }}Copy the code

With the above code we have implemented a simplified version of Vue without template compilation, which simulates dom changes with simplification.

/ / = = = = = = test = = = = = = =
let data={
    message:'hello'.num:0
}
let app=new Vue({
    data:data
});
// Simulate data listening
new Watcher(app,'message'.function(value){
    // Simulate DOM changes
    console.log('Dom changes caused by message -->,value);
})
new Watcher(app,'num'.function(value){
    // Simulate DOM changes
    console.log('dom change caused by num -->',value);
})
data.message='world';
data.num=100;
Copy the code

The above test code output

Dom changes caused by message –> WORLD num –>100

Why nextTick

After careful observation, we will find that, in accordance with the above responsive principle, there will be serious performance problems when we make frequent updates to some data. For example, we modify the num attribute above:

for(let i=0; i<100; i++){ data.num=i;// Every time the data changes, Watcher's update is called to update the DOM
}
Copy the code

The code above causes the Watcher callback for NUM to be executed frequently (100 times), which corresponds to 100 DOM updates. DOM updates are expensive in terms of performance, so we should minimize DOM operations in our development. A good Vue writer would certainly not allow this to happen, and Vue uses nextTick to optimize this problem. To put it simply, instead of performing DOM updates immediately after each data change, you need to cache the data changes and perform only one DOM update when appropriate. Here you need to set a proper time interval, which can be perfectly solved by the event loop mechanism described below.

Event loop mechanism

A simple understanding of the browser event loop mechanism means that there are two types of tasks, macro tasks and micro tasks, executed in JS code. Macro tasks are code we write that executes sequentially and tasks like setTimeout are created, while microtasks are code that executes through callback functions like promise.then. Event execution order:

  • Macro task
  • All microtasks generated by this macro task
  • Render (View update)
  • Next macro task

So this goes on and on and on, and just to make it easier to understand, let’s do a simple example.

console.log('Macro Task 1')
setTimeout(() = >{
    console.log('Macro Task 2')})Promise.resolve().then(() = >{
    console.log('Microtask 1')})Promise.resolve().then(() = >{
    console.log('Micromission 2')})Copy the code

The result of the above code is:

Macro task 1 Micro task 1 Micro task 2 Macro task 2

NextTick is a Node Event Loop, and the browser Event Loop is a Node Event Loop. As you are smart enough to see, our cache of data changes can rely on event loops; Since there is a view render between each event loop, we only need to update the DOM before rendering, so we need to cache the data changes to avoid invalid DOM operations and only save the final result of the last data change. There are two simple implementations: setTimeout and Promise. The usual setTimeout creates a macro task, while promise.then creates a microtask. The microtask created with Promise will be executed after the synchronization code execution of this event loop. The macro task created with setTimeout will also be executed after the synchronization code execution. The difference is that an invalid view rendering will be inserted before the setTimeout code execution. So we tried to use Promise to create microtasks for asynchronous updates.

The highlight: nextTick

Core principles and asynchronous update queue.

Pre-knowledge review

Speaking of the implementation of nextTick in Vue, we must mention a new concept called asynchronous update queue, where there are two keywords asynchronous, update queue. To understand this concept, we need to review how the simple Vue we wrote earlier responds to data and simulates DOM updates. Here’s the overall flow: Observe adds a proxy for data. When data is used, we can collect the Watcher object that depends on the data by using the GET proxy method and save it to the Dep as the dependency of the data. This process is called dependency collection. Then, when we modify the data, the set agent method of the data is triggered, and the notify method of the Dep triggers the update method of all dependencies to perform the update. The problem is with the update step, where we trigger updates synchronously, that is, immediately, like the for loop, which updates n times, which wastes performance, especially for DOM updates, because dom updates are expensive, Second, most of these are invalid updates that the user doesn’t see (because of the browser’s event loop, which only renders the interface once in a loop). So here we can use the browser event loop to implement asynchronous updates, with only one DOM update per event loop for the changed data. To review the event loop, let’s list the order in which the tasks in the browser event loop are executed:

  • Macro task
  • All microtasks generated by this macro task
  • Render (View update)
  • Next macro task

Train of thought to sort out

With that in mind, we can put together a general idea of how to implement asynchronous updates using the browser’s event loop, with only one DOM change per time loop.

First we created a Watcher object for each data we were looking at. When the data changed, the Update method of the Watcher object was triggered. Instead of firing the run method in the update directly, Instead, we save the changed Watcher to a queue to be updated (the array implementation), and we create a microtask for the queue to be updated to perform the updates saved in it. With that in mind, let’s start from Watcher.

Began to transform

Update (); update ();

 update(newValue){
        // Only when the value changes
        if(this.value! ==newValue){this.value=newValue;
            this.run(); }}// Perform operations such as DOM updates
    run(){
        this.cb(this.value);
    }
Copy the code

The update method executes the RUN method to update the DOM immediately after a data change is detected. We need to create a global updateQueue array as a queue to store the Watcher corresponding to the current data change. Instead of executing the run method directly in the update method, the changed Watcher object itself is added to the update queue updateQueue

    let updateQueue=[];// Note that this array is a global declaration and is no longer in the Watcher class
    update(newValue){
        // Only when the value changes
        if(this.value! ==newValue){this.value=newValue;
            // Add Watcher to the asynchronous update queue for subsequent updates
            updateQueue.push(this); }}// Perform operations such as DOM updates
    run(){
        this.cb(this.value);
    }
Copy the code

In the code above we added the changed Watcher to the update queue updateQueque for subsequent updates. Now we write a function to clear the update queue and perform the updates in sequence, which will be executed later in the microtask.

function flushUpdateQueue(){
    while(updateQueue.length>0){ updateQueue.shift().run(); }}Copy the code

Now we have a function that handles the update queue, but there is one important step that is missing: the time to execute the function. At this point, we can use the event loop mechanism mentioned above, namely using setTimeout or Promise to implement asynchronous updates. This is the nextTick code implementation. The following is a simplified implementation of the nextTick function:

let callbacks=[];// An event queue containing asynchronous DOM update queues and user-added asynchronous events
let pending=false;FlushCallbacks are executed once during each macro task to flush the callbacks
funciton nextTick(cb){
   callbacks.push(cb);
   if(! pending){ pending=true;
      // Promise can also be used here, the Promise creates a microtask, the microtask will be executed after this event loop synchronization code execution, setTimeout creates a macro task, also will be executed after this synchronization code execution, The difference is that an invalid view rendering is interspersed before the setTimeout code executes, so we try to use Promise to create microtasks for asynchronous updates.
      if(Promise) {Promise.resovle().then(() = >{ flushCallbacks(); })}else{
          setTimeout(() = >{ flushCallbacks(); })}}}function flushCallbacks(){
    pending=false;// State reset
    callbacks.forEach(cb= >{ callbacks.shift()(); })}Copy the code

We create a Callbacks array as a queue to hold events, we queue one event into the Callbacks event queue each time we call the nextTick function, and then we create an asynchronous event in setTimeout or promise.then, FlushCallbacks dequeue and execute functions in an asynchronous queue once. The pending variable is used to prevent asynchronous tasks (setTimeout or promise.then) from being created repeatedly during this synchronous (macro) task. Add the above code to the Vue class:

class Vue{
    constructor(options){
        this.waiting=false
        this.$el=options.el;
        this._data=options.data;
        this.$data=this._data;
        this.$nextTick=this.nextTick;
        new Observer(this._data);
    }
    // Easy version nextTick
    nextTick(cb){
         callbacks.push(cb);
         if(! pending){// Control variable that controls the execution of only one flushCallbacks per event loop
             pending=true;
             if(Promise) {Promise.resovle().then(() = >{
                      this.flushCallbacks(); })}else{
                  setTimeout(() = >{
                      this.flushCallbacks(); })}}}/ / empty callbacks
    flushCallbacks(){
       while(callbacks.length! =0){
         callbacks.shift()(this);// The current vue instance is passed in to get a waiting for the subsequent flushUpdateQueue
      }
      pending=false;
    }
    // Empty the UpdateQueue queue and update the view
    flushUpdateQueue(vm){
        while(updateQueue.length! =0){
           updateQueue.shift().run();
        }
        has={};
        vm.waiting=false; }}Copy the code

Further improvements to Watcher are as follows:

class Watcher{
     constructor(vm,key,cb){
        this.vm=vm;
        this.key=key;
        this.uid=uid++;
        this.cb=cb;
        // Call get to add dependencies
        Dep.target=this;
        this.value=vm.$data[key];
        Dep.target=null;
     }
     update(){
         if(this.value! = =this.vm.$data[this.key]){
             this.value=this.vm.$data[this.key];
             if(!this.vm.waiting){// Control variable to add only one flushUpdateQueue to the callbacks during each event loop
                this.vm.$nextTick(this.vm.flushUpdateQueue); 
                this.vm.waiting=true;
             }
             // Instead of executing the run method immediately, place it in the updateQueue queue
             if(! has[this.uid]){
                 has[this.uid]=true;
                 updateQueue.push(this); }}}run(){
         this.cb(this.value); }}Copy the code

Here we take the Update method of the Watcher class a step further by passing in the previously defined flushUpdateQueue to the nextTick function to update the DOM. In addition, the code adds an object HAS to ensure that no duplicate Watcher objects are added to the asynchronous update queue.

Complete source code

class Dep{
    static target=null
    constructor(){
        this.subs=[];
    }
    addSubs(watcher){
        this.subs.push(watcher)
    }
    notify(){
        for(let i=0; i<this.subs.length; i++){this.subs[i].update(); }}}class Observer{
     constructor(data){
        if(typeof data=='object') {this.walk(data); }}walk(obj){
         const keys=Object.keys(obj);
         for (let i = 0; i < keys.length; i++) {
             this.defineReactive(obj, keys[i])
         }
     }
     defineReactive(obj,key){
         if(typeof obj[key]=='object') {this.walk(obj[key]);
         }
         const dep=new Dep();
         let val=obj[key];
         Object.defineProperty(obj, key, {
             enumerable: true.configurable: true.// The get agent adds the dep. target, or Watcher object, to the dependency collection
             get: function reactiveGetter () {
               if (Dep.target) {
                 dep.addSubs(Dep.target);
               }
               return val;
             },
             set: function reactiveSetter (newVal) {
                  val=newVal;
                  dep.notify()
             } 
           })
     }
 }
 let uid=0
 class Watcher{
     constructor(vm,key,cb){
        this.vm=vm;
        this.key=key;
        this.uid=uid++;
        this.cb=cb;
        // Call get to add dependencies
        Dep.target=this;
        this.value=vm.$data[key];
        Dep.target=null;
     }
     update(){
         if(this.value! = =this.vm.$data[this.key]){
             this.value=this.vm.$data[this.key];
             if(!this.vm.waiting){// Control variable to add only one flushUpdateQueue to the callbacks during each event loop
                this.vm.$nextTick(this.vm.flushUpdateQueue); 
                this.vm.waiting=true;
             }
             // Instead of executing the run method immediately, place it in the updateQueue queue
             if(! has[this.uid]){
                 has[this.uid]=true;
                 updateQueue.push(this); }}}run(){
         this.cb(this.value); }}const updateQueue=[];// Update queue asynchronously
  let has={};// Control not to save duplicate Watcher in the change queue
  const callbacks=[];
  let pending=false;
 class Vue{
    constructor(options){
        this.waiting=false
        this.$el=options.el;
        this._data=options.data;
        this.$data=this._data;
        this.$nextTick=this.nextTick;
        new Observer(this._data);
    }
    // Easy version nextTick
    nextTick(cb){
         callbacks.push(cb);
         if(! pending){// Control variable that controls the execution of only one flushCallbacks per event loop
             pending=true;
             setTimeout(() = >{
                 // will be executed after the synchronization code (the last macro task) is completed
                 this.flushCallbacks(); }}})// Empty the UpdateQueue queue and update the view
    flushUpdateQueue(vm){
        while(updateQueue.length! =0){
           updateQueue.shift().run();
        }
        has={};
        vm.waiting=false;
    }
    / / empty callbacks
    flushCallbacks(){
       while(callbacks.length! =0){
         callbacks.shift()(this);// Pass the current VM instance to make it available to flushUpdateQueue
      }
      pending=false; }}Copy the code

test

/ / = = = = = = test = = = = = = =
let data={
    message:'hello'.num:0
}
let app=new Vue({
    data:data
});
// Simulate data listening
let w1=new Watcher(app,'message'.function(value){
    // Simulate DOM changes
    console.log('Dom changes caused by message -->,value);
})
// Simulate data listening
let w2=new Watcher(app,'num'.function(value){
    // Simulate DOM changes
    console.log('dom change caused by num -->',value);
})
data.message='world'// Once data is updated, a flushUpdateQueue callback is added to the nextTick event queue callbacks
data.message='world1'
data.message='world2'// Message changes are pushed into updateQueue and only the result of the last assignment is saved
for(let i=0; i<=100; i++){ data.num=i;// The change to num is pushed into updateQueue and only the result of the last assignment is saved
}
// An asynchronous callback event added by the developer for the callbacks
app.$nextTick(function(){
   console.log(This is what happens when you update the DOM.)})Copy the code

Let’s take a look at the sequence of the code in the above example to understand it better:

  1. Execute sync code

  2. Data. message=’world’ flushUpdateQueuepush into the Callbacks queue via nextTick, and the Watcher corresponding to the message attribute into updateQueue. Subsequent data.message updates only change the value of Watcher and do not add it again to updateQueue.

  3. NextTick also attempts to flushUpdateQueuepush into the Callbacks queue, FlushUpdateQueue will not be added again. The Watcher corresponding to the num attribute will be added to updateQueue. 99 subsequent data.num changes will not be added to updateQueue again.

  4. Next we actively implement the nextTick method of the VUE object, adding a callback function to the Callbacks queue;

  5. When the synchronous code (macro task) is complete, it is the turn of the asynchronous task in nextTick to execute. Promise.then and setTimeout:

  • Promise.then

    FlushCallbacks are executed in the newly created microtask, and flushUpdateQueue is executed in turn to execute the UI update queue and the callback function added by the developer. Dom changes are completed after the microtask is completed, followed by the browser’s view rendering.

  • setTimeout

    FlushCallbacks are executed in the newly created macro task. Since there is a browser view rendering between macro tasks, an invalid view rendering is executed first, followed by the flushUpdateQueueUI update queue in the Callbacks and the callbacks added by the developer. Dom updates are complete, followed by the next view rendering.

conclusion

The above is the introduction of the realization principle of nextTick in Vue. As the pre-knowledge, it also briefly introduces the realization principle of Vue responsiveness and js event loop mechanism. If you have gained, please like 👍, if you have not, please do not hesitate to point out. References: Vue operation mechanism