NextTick is a required part of the VUE framework for the front-end interview. It deals with the problem of not getting the latest DOM node object immediately after we change the data.

A few simple concepts before we get down to business:

What is a process:

A process is the smallest unit of resources allocated by the CPU. (The smallest unit that can have resources and operate independently)

What is a thread:

Threads are the smallest unit of CPU scheduling; (A thread is a unit of running a program at a time based on a process. There can be multiple threads in a process.)

Browsers are multi-process:

In the browser, every time a TAB page is opened, a new process is actually opened. In this process, there are also UI rendering threads, JS engine threads, HTTP request threads, etc. So, the browser is multi-process.

Js is single threaded:

Js is the scripting language of the browser, mainly to realize the interaction between the user and the browser, and operate DOM; This means that it has to be single-threaded, which can cause complex synchronization problems. For example, if js is designed to be multithreaded, the browser will look confused if one thread tries to modify a DOM element and another thread tries to delete it. So, to avoid complexity, JavaScript has been single-threaded since its inception.

Js execution mechanism –Event loop

As JS is a single thread, JS designers divide tasks into synchronous tasks and asynchronous tasks. Synchronous tasks are all executed in a queue on the main thread. The previous tasks are not completed, and the subsequent tasks will always wait. Asynchronous tasks are suspended in a task queue, waiting for the main thread to complete all tasks, and notifies the task queue that executable tasks can be put to the main thread for execution. After the asynchronous task is put into the main thread, the task queue is notified to put the next asynchronous task into the main thread for execution. This process continues until the asynchronous task completes, and is called an Event loop. And a cycle is a tick.

Asynchronous tasks in the task queue can be divided into two types: MicrotAST and MacroTask: Promise, Process. nextTick, Object.observe, MutationObserver macroTask: Script overall code, setTimeout, setInterval and other execution priorities, execute macroTask first, and then execute microtask mincroTask.

The following points need to be noted during execution:

  • In an Event loop, microTask fetches and fetches until the MicroTask queue is empty, while MacroTask fetches once at a time.
  • If an asynchronous task is added during the execution of the event loop, or if it is a MacroTask, it is placed at the end of the MacroTask and waits for the next loop to execute. If it is a Microtask, place it at the end of the MicroTask in the event loop to continue execution. Until the MicroTask queue is empty.

With these concepts out of the way, let’s move on to today’s hero: the Vue nextTick principle

Asynchronous update and nextTick principle

Vue Dom update:

In case you haven’t noticed, Vue executes asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.

So when we set this. MSG = ‘some thing’, Vue does not immediately update the DOM data, but puts the operation in a queue; If we do it repeatedly, the queue will be de-duplicated; After all data changes in the same event loop are complete, the events in the queue are taken out for processing.

This is mostly to improve performance, because if you update the DOM in the main thread, you update the DOM 100 times in 100 cycles; However, if you update the DOM after the event loop completes, you only need to update it once.

To manipulate the DOM after the data update operation, we can use vue.nexttick (callback) immediately after the data changes; This callback will be called after the DOM update is complete to retrieve the latest DOM element.

The official documentation defines nextTick as follows:

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

When the data in the page changes, the task is placed in an asynchronous queue, and DOM rendering is performed only when the current task is idle. When DOM rendering is complete, the function is automatically executed.

When a setter method for data is fired, its setter function notifies the Dep, and the Dep calls all the Watch objects it manages. Trigger the update implementation of the Watch object. Let’s take a look at how update is implemented:

/* Scheduler interface, /* Istanbul ignore else */ *computed watcher runs this render view */ if (this.lazy) {this.dirty = true } else if (this.sync) {/* Sync will execute run to render the view directly */ this.run()} else {/* Asynchronously push to the observer queue, called on the next tick. */ queueWatcher(this) } }Copy the code

As you can see from the code, queueWatcher(this) is called when data changes, which is how VUE asynchronously updates the queue. So let’s follow along and see what queueWatcher did:

QueueWatcher (watcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher Watcher) {/* get the id of Watcher */ const id = Watcher. Id /* Check whether the id exists. */ if (has[id] == null) {has[id] = true if (! flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i >= 0 && queue[i].id > watcher.id) { i-- } queue.splice(Math.max(i, index) + 1, 0, watcher) } // queue the flush if (! waiting) { waiting = true nextTick(flushSchedulerQueue) } } }Copy the code

QueueWatcher code shows that instead of updating the view immediately, the Watch object is pushed into a queue, which is in a waiting state. Wait until the next tick runs and the queue is all taken out and run, then the Watch objects will be iterated and the view will be updated. Also, a Watcher with a duplicate ID is not added to the queue more than once. This also explains why the same Watcher is triggered more than once and only gets pushed into the queue once.

Vue updates the DOM asynchronously to avoid frequent DOM manipulation. These asynchronous operations are queued as cb tasks (microtasks first) through the nextTick function, and are executed each time the tick ends, updating the DOM.

Vue creates the nextTick function to implement asynchronous updates. Let’s take a look at how nextTick works:

Const callbacks = [] / const callbacks = [] / const callbacks = [] / const callbacks = [] / const callbacks = [] / const callbacks = [] / const callbacks = [] / The pointing function will be pushed to the task queue, and when the main thread task is finished, */ let timerFunc /* execute cb callback function CTX context */ export function nextTick (cb? : Function, ctx? : Callbacks. Push (() => {if (cb) {try {cb.call(CTX)} catch (e)  { handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(CTX)}}) // Check if the last asynchronous task queue (i.e. the task array named callbacks) was dispatched and completed. Pending = if (! Pending = true // Call Promise, MutationObserver, pending = true TimerFunc ()} // Step 3 executes the returned state if (! cb && typeof Promise ! == 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }Copy the code

Let’s first go to where nextTick is defined and see what it does. You can see that it defines three variables in the outer layer, one of which is familiar from its name: callbacks, the queue we mentioned above; Defining variables in the outer layer of nextTick creates a closure, so every time we call $nextTick we’re adding a callback to the callbacks.

Pending indicates that the timerFunc function can only be executed once at a time. So what does this timerFunc function do? Let’s look at the code:

export let isUsingMicroTask = false if (typeof Promise ! == 'undefined' &&isnative (Promise)) { Resolve () timerFunc = () => {p.hen (flushCallbacks) if (isIOS) setTimeout(noop)} isUsingMicroTask = true } else if (! isIE && typeof MutationObserver ! == 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' ) {// Judgment 2: 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 (typeof setImmediate ! == 'undefined' &&mediate (setImmediate) { SetImmediate timerFunc = () => {setImmediate(flushCallbacks)}} else { TimerFunc = () => {setTimeout(flushCallbacks, 0)}}Copy the code

There are several isNative functions, which are used to determine whether the passed parameters are supported natively in the current environment. For example, some browsers don’t support Promises, and isNative(Promise) returns false even though we used a shim (Polify).

Then, MutationObserver, and setImmediate Mediate, all of which do not support the use of setTimeout. The purpose of the flushCallbacks is to place the flushCallbacks function into either the microtask (judgments 1 and 2) or the macro task (judgments 3 and 4) and wait for the next event loop to execute. MutationObserver is a new feature in Html5 that listens for changes to the target DOM structure, i.e. the newly created textNode in the code. If it changes, the callback function in the MutationObserver constructor is executed, but it is executed in a microtask.

So what exactly is flushCallbacks? NextTick is desperate to put it into microtasks or macros:

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

What it does is very simple. It copies the Callbacks array, sets the callbacks to null, and executes each function in the copied array. So it’s only used to execute callbacks.

conclusion

Now that the entire nextTick code has been analyzed, the process can be summarized as follows:

  1. Put the callback into the callbacks to wait for execution
  2. Place the execution function in a microtask or macro task
  3. Events loop to the microtask or macro task, and the executing function in turn executes the callbacks in the Callbacks

Finally, with the content of this article, we once again detail the strengthening of the understanding of official documents:

Vue is executed asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.

Point a concern, mutual supervision of learning progress!

Wechat official number: CodeLee front-end development focus for more technical articles. Questions or suggestions, please leave a message on the public account; Click “Reading” if you find the article helpful.