preface

In my last column, I mentioned that the subscriber response is to add the subscriber to a queue, and then the nextTick function iterates through the queue, processing the response for each subscriber. Both the vue. nextTick global method and the vm.$nextTick instance method of the familiar Vue API internally call the nextTick function, which can be understood as asynchronously executing the incoming function.

Vue. NextTick internal logic

Vue.nexttick is defined this way in initializing the Vue global API by executing initGlobalAPI(Vue).

function initGlobalAPI(Vue) {
    / /...
    Vue.nextTick = nextTick;
}
Copy the code

You can see that you assign the nextTick function directly to vue.nexttick, and that’s it. It’s very simple.

Vm.$nextTick

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)};Copy the code

The nextTick function is also called inside vm.$nextTick.

Third, pre-knowledge

The nextTick function can be understood as asynchronous execution of the incoming function. Here is an introduction to asynchronous execution, starting with the JS runtime mechanism.

1. JS operation mechanism

JS execution is single-threaded, called single thread is the event queues in task execution, before the end of a task, to perform a task, after this is the synchronization task, in order to avoid a task executes for a long time before hasn’t ended, the next task will not be able to perform, introduced the concept of asynchronous tasks. JS running mechanism can be simply described in the following steps.

  • All synchronization tasks are executed on the main thread, forming an execution Context stack.
  • In addition to the main thread, there is a task queue. Whenever an asynchronous task has a run result, its callback function is added as a task to the task queue.
  • Once all synchronization tasks in the execution stack have been executed, the task queue is read to see which tasks are in it, added to the execution stack, and execution begins.
  • The main thread repeats step 3 above. This is also known as an Event Loop.

Type of asynchronous task

The nextTick function asynchronously executes the passed function and is an asynchronous task. There are two types of asynchronous tasks.

The main thread is executed as a tick, and all asynchronous tasks are executed through a task queue. Tasks are stored in a task queue. According to the specification, tasks are divided into two categories: Macro task and micro task. After each Macro task is finished, all micro tasks should be cleared.

Visualize the order in which tasks are executed with a piece of code.

for (macroTask of macroTaskQueue) {
    handleMacroTask();
    for (microTask ofmicroTaskQueue) { handleMicroTask(microTask); }}Copy the code

Common ways to create a Macro Task in a browser environment are

  • SetTimeout, setInterval, postMessage, MessageChannel(queue takes precedence over setTimeiout execution)
  • Network Request IO
  • Page interaction: DOM, mouse, keyboard, scroll events
  • Page rendering

Common ways to create Micro Tasks

  • Promise.then
  • MutationObserve
  • process.nexttick

The nextTick function uses these methods to process functions passed in with cb as asynchronous tasks.

NextTick function

var callbacks = [];
var pending = false;
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();
    }
    if(! cb &&typeof Promise! = ='undefined') {
        return new Promise(function(resolve) { _resolve = resolve; }}})Copy the code

You can see that in the nextTick function you wrap the function that was passed in with the cb parameter and then push it into the Callbacks array.

The variable pending is then used to ensure that timerFunc() is executed only once in an event loop.

Finally, execute if (! cb && typeof Promise ! == ‘undefined’), if cb does not exist and the browser supports Promise, return a Promise class instantiation object. For example, nextTick().then(() => {}), when _resolve is executed, then logic is executed.

Take a look at the timerFunc function definition, starting with a ztimerFunc function that uses Promise to create an asynchronous execution.

var timerFunc;
if (typeof Promise! = ='undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop); }}; }Copy the code

It turns out that the timerFunc function simply calls the flushCallbacks function with various asynchronously executed methods.

Take a look at the flushCallbacks function

var callbacks = [];
var pending = false;
function flushCallbacks() {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) { copies[i](); }}Copy the code

Execute pending = false to call the timerFunc function from the nextTick function in the next event loop.

Var copies = callbacks.slice(0); callbacks.length = 0; Clone the set of callbacks to be executed asynchronously into constant copies and empty the callbacks.

And then execute each function through copies. The return to nextTick is to wrap the function passed in with the cb parameter and push it into the Callbacks collection. Let’s see how it’s packaged.

function() {
    if (cb) {
        try {
            cb.call(ctx);
        } catch (e) {
            handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }}Copy the code

The logic is simple. If cb has a value. Cb.call (CTX) is executed in the try statement, with CTX being the argument to the function passed in. If execution fails, handleError(e, CTX, ‘nextTick’) is executed.

If cb has no value. Execute _resolve(CTX), since the nextTick function returns a Promise class instantiation object if cb has no value, then execute _resolve(CTX), which will execute the then logic.

At this point the mainline logic of the nextTice function is clear. Define a variable callbacks, wrap the function passed through cb in a function that executes the passed function, handles the failure and non-existence of cb, and then adds it to the callbacks. Call the timerFunc function, where each function is executed through callbacks, because timerFunc is a function that executes asynchronously, and define a variable pending to ensure that the timerFunc function is called only once in an event loop. This enables the nextTice function to execute the incoming function asynchronously.

So the key here is how to define the timerFunc function. Since there are different ways to create asynchronous execution functions in different browsers, there are several ways to make them compatible.

1. Promise creates an asynchronous execution function

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
    isUsingMicroTask = true;
}
Copy the code

Execute if (typeof Promise! == ‘undefined’ && isNative(Promise))

Typeof Promise supports function, not undefined, so this condition is satisfied, this condition is easy to understand.

Let’s look at another condition, where the isNative method is defined, in the following code.

function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
Copy the code

If Ctor is a function, /native code/.test(ctor.tostring ()) is executed to check whether the string after toString contains a native code fragment. Function Promise() {[native code]} returns a Function Promise() {[native code]}.

Var p = promise.resolve () if the browser supports it. The promise.resolve () method allows the call to return an Resolved Promise object with no arguments.

In timerFunc, p.chen (flushCallbacks) executes flushCallbacks directly, iterating through each function passed in by nextTick, Because the Promise is a micro Task type, these functions are executed asynchronously.

Execute if (isIOS) {setTimeout(noop)} to add an empty timer in the IOS browser to force the refresh of the microtask queue.

MutationObserver creates asynchronous execution functions

if (! isIE && typeof MutationObserver ! == 'undefined' && (isNative(MutationObserver) || 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; }Copy the code

MutationObserver() creates and returns a new MutationObserver, which is called when the specified DOM changes. IsIE excludes Internet Explorer. Execute typeof MutationObserver! == ‘undefined’ && (isNative(MutationObserver)), the principle of which has been introduced above. Perform MutationObserver. ToString () = = = ‘[object MutationObserverConstructor]’) this is for PhantomJS browser and iOS 7. X version of the browser support for judgment.

Var Observer = new MutationObserver(flushCallbacks) Create a new MutationObserver, assign it to the constant Observer, and pass in flushCallbacks as a return function. The flushCallbacks function is called when the DOM specified by the observer changes the property to listen for.

Create a textNode by calling var textNode = document.createtextnode (String(counter)).

Var counter = 1, counter is the content of the text node.

Observe (textNode, {characterData: true}) and call MutationObserver method observe to listen to the textNode textNode.

It’s very clever to use counter = (counter + 1) % 2, to make it go between 1 and 0. Textnode.data = String(counter) sets the changed counter to the contents of the textNode. The observer then detects that the contents of the text node it is observing have changed and calls the flushCallbacks function, which iterates through to execute each function passed in by nextTick. Since MutationObserver is a micro Task type, these functions are executed asynchronously.

SetImmediate Creates asynchronous mediate

if (typeof setImmediate ! == 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(flushCallbacks); }; }Copy the code

SetImmediate is only compatible with IE10 and beyond, and not with any other browsers. It is a macro task and consumes relatively small resources

SetTimeout creates an asynchronous execution function

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

Compatible with Internet Explorer 10 or less, create asynchronous tasks, which are macro tasks and consume large resources.

5. Create the order in which functions are executed asynchronously

The order in which Vue implements timerFunc in the nextTick function has been tweaked a few times throughout history until 2.6+

In the first version of nextTick, timerFunc was implemented in the following order: Promise, MutationObserver, and setTimeout.

The order in which timerFunc is implemented in version 2.5.0 was changed to setImmediate, MessageChannel, and setTimeout. In this release, all the methods for creating microtasks are removed due to the high priority of microtasks. One of the issues numbered #6566 is as follows:

<div class="header" v-if="expand"> // block 1
    <i @click="expand = false;">Expand is True</i> // element 1
</div>
<div class="expand" v-if=! "" expand" @click="expand = true;"> // block 2
    <i>Expand is False</i> // element 2
</div>
Copy the code

Logical clicking on Element 1 would set expand to false, block 1 would not display, block 2 would display, and clicking on Block 2 would set expand to false, and block 1 would display.

What happened was that clicking on Element 1 would only show block 1. This is why and what causes this BUG. Vue officially explained it this way

Click events are macro tasks, and click events on < I > trigger the first update on nextTick. Process the microtask before the event bubbles to the external div. During the update, a Click listener is added to the external div. Because the DOM structure is the same, both external divs and internal elements are reused. The event eventually reaches the external div, triggering the listener added by the first update, which in turn triggers the second update. To solve this problem, you can simply give the two external divs different keys to force them to be replaced during update. This will prevent bubbling events from being received.

The solution, of course, was to change timerFunc to create a macro task instead, using setImmediate, MessageChannel, and setTimeout in the order that nextTick is a macro task.

The click event is a macro task, and updates on the nextTick triggered when the click event is finished will only occur in the next event loop, so that the event bubbling has already completed. You don’t get the BUG.

But after a while, the order of implementation of timerFunc was changed to Promise, MutationObserver, setImmediate, and setTimeout. Using the macro task anywhere can lead to some pretty bizarre problems, with issue #6813, I’m going to type the code, and you can see it here. There are two key controls

  • For media queries, li displays inline boxes when the page width is greater than 1000px, and block-level elements when the page width is less than 1000px.
  • Listen for page zooming, which ul uses when the page width is less than 1000pxv-show="showList"Control hide.

Initial state:

When you drag the border of a page quickly to reduce the width of the page, the first image below will appear and then quickly hide, rather than hide directly.

In order for this BUG to occur, the first thing to understand is the concept of UI Render execution time, as shown below:

    1. Macro takes a macro task.
    1. Micro Clears the micro task queue.
    1. Check whether the current frame is worth updating. Otherwise, go to Step 1 again
    1. Before a frame is drawn, the requestAnimationFrame queue task is executed.
    1. UI update, perform UI Render.
    1. If the macro task queue is not empty, re-enter the step

The process is also easier to understand. Previously, performing the listening window scaling was a macro task. When the window size is less than 1000px, the showList will change to Flase and trigger a nextTick execution, which is a macro task. In the next macro task, the nextTick macro task is executed. When UI Render is performed again, ul’s display value is changed to None, and the list is hidden.

So Vue felt that the controllability of The nextTick created with microtasks was fine, unlike the uncontrollable scenarios created with macro tasks.

In version 2.6 +, a timestamp was used to fix the #6566 BUG. The attachedTimestamp variable is set to “flushSchedulerQueue” in the nextTick function. CurrentFlushTimestamp = getNow() to get a timestamp assigned to the variable currentFlushTimestamp, and then to do a hijacking before listening for events in the DOM. This is implemented in the add function.

function add(name, handler, capture, passive) {
    if (useMicrotaskFix) {
        var attachedTimestamp = currentFlushTimestamp;
        var original = handler;
        handler = original._wrapper = function(e) {
            if (
                e.target === e.currentTarget ||
                e.timeStamp >= attachedTimestamp ||
                e.timeStamp <= 0|| e.target.ownerDocument ! = =document
            ) {
                return original.apply(this.arguments)}}; } target.addEventListener( name, handler, supportsPassive ? {capture: capture,
            passive: passive
        } : capture
    );
}
Copy the code

Execute if (useMicrotaskFix), which is set to true when creating asynchronous execution functions with microtasks.

AttachedTimestamp = currentFlushTimestamp Assigns the timestamp of the nextTick callback to attachedTimestamp, Then run if(e.timstamp >= attachedTimestamp). The event will be executed only when the timestamp of the event on e.timstamp DOM is greater than attachedTimestamp.

Why? Back to BUG #6566. Due to the high execution priority of Micro Task, the #6566 BUG occurs when it bubbles faster than events. When clicking on the I tag, the bubble event is triggered earlier than nextTick execution, so e.timstamp is smaller than attachedTimestamp and causes BUG #6566 if the bubble event is allowed to execute. Therefore, the BUG can be avoided only when the bubbler event is triggered later than nextTick. Therefore, the bubbler event can be executed only when e.timstamp is larger than attachedTimestamp.