This is the 21st day of my participation in the August Text Challenge.More challenges in August

👉 Sunset and lone duck fly together, autumn water together long sky color. Preface to The Pavilion of The Genu King by Tang Wangbo

preface

NextTick is the core of Vue asynchronous update strategy, and also involves excellent test points such as Event-loop, which is a rare comprehensive question setting direction in Vue interview.

What the hell is nextTick?

As we know, Vue is an asynchronous update, that is, if you fire an event like this:

methods: { 
    update() {
        for (let i = 0; i < 10; i++) { 
            this.testNum = this.testNum + i; }}}Copy the code

In your Vue view, testNum will change. Note, however, that although we changed the firstNum loop 10 times, it actually only updated the last value to the view — which is also quite reasonable. When a data update occurs, it doesn’t immediately give you the view layer update action. Instead, you “save” the update and wait until “the time is right” to execute it. This “save” place is called the asynchronous update queue; Even if a Watcher is triggered more than once, it will only be pushed into the asynchronous update queue once. After the synchronization logic is complete, watcher corresponds to the latest values of its dependent properties. Finally, Vue dequeues the asynchronous update queue for batch updates.

This interface for asynchronous task delivery is called nextTick.

Vue-nexttick source code overview

Vue/SRC /core/util/next-tick.js:

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false
const callbacks = []

let pending = false

function flushCallbacks () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}
// The function used to dispatch asynchronous tasks

let timerFunc

if (typeof Promise! = ='undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () = > {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {
    let counter = 1
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () = > {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    timerFunc = () = > {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () = > {
        setTimeout(flushCallbacks, 0)}}export function nextTick (cb? :Function, ctx? :Object) {
    let _resolve
    callbacks.push(() = > {
        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(resolve= > {
            _resolve = resolve
        })
    }
}
Copy the code

In the code above, there are three key roles: nextTick, timerFunc, and flushCallbacks. First, nextTick is the entry function and protagonist, so let’s see what it does:

// Expose the nextTick method
export function nextTick (cb? :Function, ctx? :Object) { 
    let _resolve
    // Maintain an asynchronous update queue
    callbacks.push(() = > { 
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')}}else if (_resolve) {
            _resolve(ctx)
        }
    })
    // Pending is a lock that ensures that tasks are executed in an orderly, non-repetitive manner
    if(! pending) { pending =true 
        timerFunc()
    }
    // Handle the case where the input parameter is not a callback
    if(! cb &&typeof Promise! = ='undefined') { 
        return new Promise(resolve= > {
            _resolve = resolve
        })
    }
}
Copy the code

There are three variables that need attention:

  • Callbacks – Asynchronously update queues
  • Pending – “lock”
  • TimerFunc – Dispatch function for asynchronous tasks

Let’s go through the logic by following the code:

First, the nextTick input is a callback function, and that callback function is a “task.” Each time nextTick receives a task, it does not immediately execute it, but pushes it into the callbacks asynchronous update queue (that is, saves it). Next, check the pending value. What’s so cool about pending? You know, I’m not going to be able to keep putting stuff in this asynchronous update queue, so I’m going to have to find a time to send it out, right? So how do you decide when to distribute it?

What does pending mean if it is false? TimerFunc = timerFunc; timerFunc = timerFunc; timerFunc = timerFunc What if pending is true? This means that the callbacks have now been dispatched, and the callbacks are already in the browser’s asynchronous task queue to ensure that they will be executed, so there is no need to execute timerFunc again to re-dispatch the queue, just add tasks to it.

Asynchronous task dispatcher — timerFunc

In this process, we mentioned two key actions — “dispatch” and “execute.” Distributing and executing detailed words are two concepts.

“Dispatch” means I throw your callbacks into the browser’s overall asynchronous queue to wait for execution — notice the “wait for execution”, when the dispatch is complete, the task queue is not executed, it just enters the waiting state.

To understand the distribution in detail, take a closer look at what timerFunc does:

let timerFunc

if (typeof Promise! = ='undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () = > {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {
    let counter = 1
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () = > {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    timerFunc = () = > {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () = > {
        setTimeout(flushCallbacks, 0)}}Copy the code

You’ll notice that the different TimerFuncs have one thing in common — they all issue the flushCallbacks function (which we’ll cover below). So what’s the difference between different timerFunc’s? We can see the difference in the way flushCallbacks are issued. There are four ways to issue flushCallbacks:

  • Promise.then
  • MutationObserver
  • setImmediate
  • setTimeout

There’s a lot of browser compatibility in this whole trove of code, so don’t go into the details. Here’s how to break the key logic down to pseudocode. It’s actually quite simple:

if(When the front environment is heldPromise) {Promise.then issue timerFunc}else if{MutationObserver issues timerFunc}else ifSetImmediate {setImmediate dispatcher timerFunc}else {
    setTimeoutDispatch timer Func}Copy the code

Then, MutationObserver, setImmediate, and setTimeout may be used to distribute timerFunc. This is an interesting priority, as you can see it is micro task first and macro task second. Notice, here’s a problem:

whyVueThe priority distribution ismicro-task?

The execution flow of event-loop looks like this:

1. Execute and dispatch a Macro-task.

2. The global context (script tag) is pushed onto the call stack to synchronize code execution. During execution, new Macro-tasks and micro-tasks can be created by calling some interfaces, which are pushed to their respective task queues. This process is essentially the execution and dequeuing of the macro task of the queue.

3. In the last step, we assigned a macro-task. In this step, we dealt with a micro-task. But it’s important to note that when Macro-Task is out, tasks are executed one by one; Micro-tasks, on the other hand, are executed in teams. Therefore, we process the micro queue by executing the tasks in the queue one by one and de-queuing them until the queue is empty.

4. Perform rendering operations to update the interface;

5. Check whether Web worker tasks exist. If so, process them.

If I send out a macro-task in 2, when will the task be executed?

If you think about it, is it going to be executed at 1 in the next loop? This is a problem — if my task is to update the UI, it is not sensed in my current loop 4; You have to wait for it to work on 1 in the next loop before it can be sensed and updated on 4 in the next loop.

Macro – Task dispatch causes our interface update to delay an event loop. At 4 of the current loop, this render time was somewhat “wasted” and it didn’t render in time for our update.

Also, micro-Tasks are updated in teams, while Macro-Tasks are updated one by one. Micro-task is also better in terms of update efficiency.

FlushCallbacks task executor

function flushCallbacks () {
    // Open the lock
    pending = false
    // Create a copy of callbacks to avoid side effects
    const copies = callbacks.slice(0)
    // The callbacks queue is empty
    callbacks.length = 0
    // Perform asynchronous tasks one by one
    for (let i = 0; i < copies.length; i++) { 
        copies[i]()
    }
}
Copy the code

It takes the tasks out of the Callbacks one by one and executes them one by one.

Notice that the first thing we do when we get into flushCallbacks is set pending to false. When flushCallbacks are finished, the callbacks will be flushed and the browser will no longer have Vue asynchronous tasks in its queue. The pending task must be null to ensure that the next Vue asynchronous task queue can be dispatched in time when it comes in.