Interpretation of the background

  1. In learning vUE source code, nextTick method achieves asynchronous update with the help of the event loop of the browser.
  2. In the interview of the company, bi liked to ask questions about the running mechanism of JavaScript, Promise/A+ and event loop thread.
  3. Learning the nextTick principle will help you locate bugs and make you more flexible with Vue.

What is an Event loop

Let’s start with a picture (from Mr. Z)

  1. After the synchronization task is executed, the next one is executed. After the synchronization task is executed, the asynchronous task is executed. When an asynchronous task is executed, the callback function of the asynchronous task is registered in the asynchronous task queue. Note that microtasks of asynchronous tasks are called directly if there are no synchronous tasks on the main thread.
  2. Perform macro tasks and add microtasks to the microtask queue.
  3. The microtask queue starts to be executed. When the macro task is executed, the microtask queue is executed until all the microtask queues are executed and the microtask queue is empty.
  4. Execute the macro task. If there are micro tasks during the execution of the macro task, add the micro tasks to the micro task queue. Execute the micro tasks after the execution of the macro task until the micro task queue is complete.
  5. Continue to execute the macro task queue.

Repeat 2, 3, 4,5… Until the macro and micro tasks are empty.

Implementation principle of $nextTick

A tick comes from a timer that periodically interrupts (outputs pulses) to indicate one tick, also known as a “clock tick”. The nextTick is literally the next tick of the clock. In Vue 2.x, nextTick is a separate file in SRC \core\util, next-tick.js, which shows the importance of nextTick. Although it is only 200 lines long, it is especially important to create a separate file to maintain.

Now let’s look at the entire file.

  1. Declare three global variables: callbacks: [], Pending: Boolean, timerFunc: undefined
  2. FlushCallbacks, a function, is declared.
  3. A bunch of **if, else if ** judgment.
  4. A function, nextTick, is thrown.

NextTick function

  1. Declare a local variable _resolve.
  2. Push all callbacks into callbacks and store all callbacks on a stack.
  3. When pending is false, the timerFunc function is executed.
  4. When no callback is available, return a Promise call that can be received with.then.

TimerFunc function

So we started saying that timerFunc is a global variable, now we call timerFunc, when is timerFunc assigned to a function, and what code is executed in that function?

As you can see, there are four branches of this code, each of which has different assignments to timerFunc. Let’s look at the first branch.

Promise branch

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}
Copy the code
  1. Determine whether the environment supports promises and whether promises are native.
  2. Use Promise to call the flushCallbacks function asynchronously.
  3. When the execution environment is iPhone, etc., use setTimeout to call NOOP asynchronously. In some abnormal WebViews in iOS, setTimeout is forced to refresh the task queue because the task queue is not refreshed after the promise ends.

MutationObserver branch

else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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
}
Copy the code
  1. Decide on non-IE browsers and whether you can use MutationObserver, a new FEATURE of HTML5.
  2. Instantiate a MutationObserver object that listens for browser DOM changes and automatically triggers a callback when the MutationObserver object is instantiated and the observe object is executed to set the DOM node to change.
  3. Assign timerFunc to a method that changes the DOM node. When the DOM node changes, the flushCallbacks are triggered. (The idea here is to use the MutationObserver feature for asynchronous operations.)

SetImmediate branch

else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
}
Copy the code
  1. To determine whether setImmediate exists, setImmediate is supported only by the higher versions of Internet Explorer (IE10+) and Edge.
  2. If so, pass into flushCallbacks to perform setImmediate.

SetTimeout branch

else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}Copy the code
  1. When none of the above branch asynchronous apis support flushCallbacks, use macro Task’s setTimeout to execute flushCallbacks.

Perform the drop

As we can see, assigning to timerFunc is a degraded process. Why? Because in the process of Vue execution, the execution environment is different, so it has to adapt to the environment.

This chart gives us a clearer picture of the downgrading process.

FlushCallbacks function

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

Loop through, executing all callbacks one by one on a first-in, first-out basis for the queue data structure.

conclusion

The principle of nextTick is to use the Event loop Event thread to asynchronously re-render. The reason for selecting Promise is that when the synchronous JS code completes execution, the execution stack will first check whether the Micro Task queue is empty. Not empty performs the microtask first. We asynchronously rerender the DOM when our DOM dependencies change, but things like Echarts, Canvas… These Vues cannot collect the dependent DOM in its initial state, so we need to manually execute the nextTick method to re-render it.