Analysis of the asynchronous rendering mechanism, nextTick principle and how to change to synchronous rendering in VUE

1. What is asynchronous rendering?

  This question should be supplemented with a premise first. We know why the response operation of page subscription does not completely correspond to the data change when the data is changing synchronously, but after all data change operations are completed, the page will receive Response, complete page rendering.

Let's experience the asynchronous rendering mechanism from an example.

import Vue from 'Vue'
new Vue({
 el: '#app',
 template: '<div>{{val}}</div>', 
 data () {  return {   val: 'init'  } },
 mounted () { 
  this.val = 'I'm rendering the page for the first time'  // debugger  
  this.val = 'I am the second page rendering'  
  const st = Date.now()  
  while(Date.now() - st < 3000) {}
 }
})

In the above code, the val attribute is assigned twice in the mounted. If the page rendering is completely synchronized with the data changes, the page should be rendered twice in the mounted.

  However, due to the asynchronous rendering mechanism inside Vue, the page will only be rendered once, and the response brought by the first assignment is combined with the response brought by the second assignment, and the final val is only Do a page rendering, and the page is not rendered until all synchronous code is executed.

After the while blocking code in the above example, the page will be rendered, just like the execution of the callback function in the familiar setTimeout, this is asynchronous rendering. Students who are familiar with React should quickly think that when the setState function is executed multiple times, the rendering of the page render is triggered, which is actually similar to the asynchronous rendering of Vue mentioned above.

2. Why do you need asynchronous rendering?

We can approach this question from both user and performance perspectives.

  From the perspective of user experience, it can also be seen from the above example that in fact our page only needs to display the second value change, the first time is just an intermediate value, if it is displayed to the user after rendering, the page will have a flickering effect , but will cause a bad user experience.

  From a performance point of view, the final data to be displayed in the example is actually the value assigned to val for the second time. If the first assignment also requires page rendering, it means that the page needs to be rendered once before the second final result rendering. Rendering will undoubtedly increase the performance consumption.

For browsers, in the case of data changes, whether it is caused by redrawing or re-rendering, it may cause inefficient page performance under performance consumption, and even cause loading problems.

Asynchronous rendering and the familiar throttling function have the same ultimate purpose. The response changes caused by multiple data changes are collected and combined into one page rendering, so as to make more reasonable use of machine resources and improve performance and user experience.

3. How to implement asynchronous rendering in VUE?

  To summarize the principle first, asynchronous rendering in Vue is actually to put the part of the page that needs to be changed into an asynchronous API callback function every time the data changes, and the asynchronous callback starts to execute after the synchronous code is executed. , and finally merge all the parts of the synchronization code that need to be rendered and changed, and finally perform a rendering operation.

  Take the above example, when val is assigned for the first time, the page will render the corresponding text, but the actual rendering changes will be temporarily stored. When val is assigned for the second time, the changes that will be caused will be temporarily stored again. It is thrown into the callback function of the asynchronous API, Promise.then. After all the synchronous code is executed, the callback function of the then function is executed, and then it will traverse the global array that stores the data changes, and determine the priority of the data in all the arrays. , and finally merge into a set of data that needs to be displayed on the page, and perform page rendering operations.

  After the asynchronous queue is executed, the global array that stores page changes is traversed and executed. During execution, some screening operations will be performed to process the data that has been repeatedly manipulated. Set of data rendering.

The asynchronous API that triggers rendering here gives priority to Promise, followed by MutationObserver. If there is no MutationObserver, setImmediate will be considered. If there is no setImmediate, the last consideration is setTimeout.

Next, we will sort out the asynchronous rendering process of Vue at the source code level.

Next, let's analyze it step by step from the perspective of source code:

1. When we use this.val='343' to assign value, the setter function of Object.defineProperty bound to the val property is triggered, and the setter function triggers the execution of the subscribed notify function.

defineReactive() { 
 ... set: function reactiveSetter (newVal) { 
  ...  dep.notify(); 
 ... } 
 ...}

2. In the notify function, execute the update method in all subscription component watcher.

Dep.prototype.notify = function notify () { 
    // copy all components watcher
    var subs = this.subs.slice(); 
    ... 
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

3. After the update function is executed, by default, lazy is false, and sync is also false. It directly enters the queueWatcher function that stores all the response changes in the global array.

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  }
else {     queueWatcher(this);

  }
};

4. In the queueWatcher function, the watcher of the component will be stored in the global array variable queue first.

By default, config.async is true, and it directly enters the function execution of nextTick. nextTick is a method implemented by the browser asynchronous API. Its callback function is the flushSchedulerQueue function.

function queueWatcher (watcher) { 
  ...
  // Store changes to be responded to in a global queue update function   queue.push(watcher);   ...
  // when async configuration is false When the page update is synchronous   if (!config.async) {     flushSchedulerQueue();
    return
  }   // Put page update function in async API Execute in the synchronous code, and start executing the update page function after the synchronous code is executed.   nextTick(flushSchedulerQueue);

}

5. After the execution of the nextTick function, the incoming flushSchedulerQueue function is push ed into the callbacks global array again. The pending is false in the initial case, and the timerFunc will be triggered at this time.

function nextTick (cb, ctx) { 
    var _resolve; 
    callbacks.push(function () { 
      if (cb) { 
      try { 
        cb.call(ctx); 
      } 
      catch (e) { 
        handleError(e, ctx, 'nextTick'); 
      } 
    } else if (_resolve) { 
      _resolve(ctx);
    }
  });
  if (!pending) {     pending = true;

    timerFunc();
  } // $flow-disable-line   if (!cb && typeof Promise !== 'undefined') {     return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

6. The timerFunc function is implemented by the browser's asynchronous APIs such as Promise, MutationObserver, setImmediate, and setTimeout. The callback function of the asynchronous API is the flushCallbacks function.

var timerFunc;
// here Vue Internal for async API selection, by Promise,MutationObserver,setImmediate,setTimeout take one // The rules used are Promise Existence Promise,does not exist MutationObserver, // MutationObserver does not exist setImmediate,setImmediate does not exist setTimeout. if (typeof Promise !== 'undefined' && isNative(Promise)) {   var p = Promise.resolve();
  timerFunc = function () {     p.then(flushCallbacks);     if (isIOS) { setTimeout(noop); }
  };   isUsingMicroTask
= true; } else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||   // PhantomJS and iOS 7.x   MutationObserver.toString() === '[object MutationObserverConstructor]')) {   var counter = 1;   var observer = new MutationObserver(flushCallbacks);   var textNode = document.createTextNode(String(counter));   observer.observe(textNode, {characterData: true});   timerFunc = function () {     counter = (counter + 1) % 2;     textNode.data = String(counter);   };   isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {   timerFunc = function () {     setImmediate(flushCallbacks);   }; } else {

  timerFunc = function () {     setTimeout(flushCallbacks, 0);   }; }

7. The flushCallbacks function will traverse and execute the global callback array of push in nextTick. The global callback array is actually the execution function of the flushSchedulerQueue of the push in step 5.

// Will nextTick inside push go in flushSchedulerQueue function for call in a loop
function flushCallbacks () { 
  pending = false; 
  var copies = callbacks.slice(0); 
  callbacks.length = 0; 
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

8. In the flushSchedulerQueue function executed by callback traversal, the flushSchedulerQueue is first prioritized according to id, and then the global queue of the stored watcher object in step 4 is traversed and executed, triggering the rendering function watcher.run.

function flushSchedulerQueue () {
  var watcher, id;
  // Install id Start sorting from small to large, the smaller the trigger is.
  updatequeue.sort(function (a, b) { 
    return a.id - b.id;
  });   
// queue is the global array, which is in queueWatcher function, every time update When triggered, the current watcher,push go in   for (index = 0; index < queue.length; index++) {     ...
    watcher.run();     
// render ...   }

}

9. The implementation of watcher.run is on the constructor Watcher prototype chain. In the initial state, the active property is true, and the set method of the Watcher prototype chain is directly executed.

Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();
    ...
  }
};

10. In the get function, push the instance watcher object to the global array, and start calling the getter method of the instance. After execution, pop the watcher object from the global array and clear the rendered dependent instances.

Watcher.prototype.get = function get () { 
  pushTarget(this); 
  // the instance push to the global array targetStack 
  var vm = this.vm; 
  value = this.getter.call(vm, vm); 
  ...
}

11. The getter method of the instance is actually the function passed in when it is instantiated, that is, the real update function _update of the following vm.

function () {
  vm._update(vm._render(), hydrating);
};

12. After the instance's _update function is executed, the two virtual nodes will be passed to the patch method of the incoming vm to perform the rendering operation.

Vue.prototype._update = function (vnode, hydrating) { 
  var vm = this; 
  ...
  
var prevVnode = vm._vnode;   vm._vnode = vnode;   if (!prevVnode) {     // initial render     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);   } else {     // updates     vm.$el = vm.__patch__(prevVnode, vnode);   }   ...
};

Fourth, nextTick implementation principle

First of all, nextTick is not an asynchronous API provided by the browser itself, but an asynchronous encapsulation method in Vue using the native asynchronous API provided by the browser itself. The above paragraphs 5 and 6 are its implementation source code. Its selection rules for the browser's asynchronous API are as follows. Promise exists by Promise.then, if Promise does not exist, it uses MutationObserver, MutationObserver does not exist setImmediate, and setImmediate does not exist. Finally, setTimeout is used to achieve it.

It can also be seen from the above access rules that nextTick may be a micro task or a macro task. From the priority of Promise and MutationObserver, it can be seen that nextTick prioritizes micro tasks, followed by setImmediate and setTimeout macro tasks.

The difference between micro-tasks and macro-tasks is not in-depth here. Just remember that after the synchronization code is executed, the micro-tasks are executed first, and the macro-tasks are executed second.

5. Can VUE render synchronously?

1, Vue.config.async = false

  Of course, it is possible. In the fourth source code, we can see the following paragraph. When the value of async in config is false, the flushSchedulerQueue is not added to the nextTick, but the flushSchedulerQueue is directly executed. It is equivalent to synchronous rendering of the page when the value in this data changes.

function queueWatcher (watcher) { 
  ...
  // Store changes to be responded to in a global queue update function
  queue.push(watcher); 
  ...
  // when async configuration is false When the page update is synchronous 
  if (!config.async) { 
    flushSchedulerQueue();
    return
  } 
  // Put page update function in async API Execute in the synchronous code, and start executing the update page function after the synchronous code is executed.
  nextTick(flushSchedulerQueue);
}

In our development code, you only need to add the next sentence to make your page rendering synchronously.

import Vue from 'Vue'
Vue.config.async = false

2,this._watcher.sync = true

In the Watch's update method execution source code, you can see that when this.sync is true, the rendering at this time is also synchronous.

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else { 
    queueWatcher(this);
  }
};

  In the development code, the sync attribute of the watcher needs to be modified to true. For the sync attribute change of the watcher, it is only necessary to execute this._watcher.sync=true before the data change operation that needs to be rendered synchronously. At this time, the page will be executed synchronously. render action.

In the following way of writing, the page will render val as 1, but not 2, and the final rendering result is 3, but the official website does not recommend this usage, please use it with caution.

new Vue({ 
  el: '#app',
  sync: true, 
  template: '<div>{{val}}</div>', 
  data () {  return { val: 0 } }, 
  mounted () { 
    this._watcher.sync = true 
    this.val = 1
    debugger     this._watcher.sync = false     this.val = 2

    this.val = 3
  }

})

So far, we have understood the reasons why Vue uses asynchronous rendering of pages, and deeply analyzed the entire operation link before rendering from the perspective of source code. At the same time, we have analyzed the direct connection between the implementation of the asynchronous method nextTick in Vue and the native asynchronous API. Finally, I learned from the source code point of view that Vue is not incapable of synchronous rendering. When synchronous rendering is required in our page, appropriate configuration can be satisfied.

Posted by rsnell on Tue, 03 May 2022 22:32:57 +0300