NextTick source code is in SRC /core/util/next-tick.js.
In vue’s next-tick implementation, we use several cases to delay calling this function. First, we determine whether our device supports the Promise object, and if so, we use promise.then to delay calling the function. So if the device doesn’t support the Promise object, then whether or not we support MutationObserver, if we do, then we use MutationObserver to delay, and if not, we use setImmediate, and if not setImmediate, SetTimeout is used to delay operations.
Thus, setImmediate and setTimeout, two macro tasks, can be thought of as degrading processing and are not typically used.
Event Loop in JS
As we all know, javascript is single-threaded, and all tasks are executed in the main thread. When all tasks in the main thread are completed, the system reads the events in the task queue “in sequence”, so the corresponding asynchronous tasks enter the main thread and start executing.
However, asynchronous task queues are divided into macrotasks and microtasks. They each have the following apis:
- Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI Rendering, etc.
- Microtasks: Promise, process.nextTick, MutationObserver, etc.
The function of the Promise’s then method is pushed into the microTasks queue (the Promise itself executes synchronously), and the setTimeout function is pushed into the MacroTasks queue. Macrotasks extract only one execution per event loop, while Microtasks extract until the MicroTasks queue is empty.
That is, if a microtask is pushed to execution, when the main thread task completes, the next task in the queue is called to execute until the task queue reaches the last task. The event loop pushes only one macrotask at a time, and the main thread checks the microtasks queue to see if there are any unexecuted tasks until all the macrotasks are completed. Loop until all asynchronous tasks are complete.
Now let’s look at a simple example:
console.log(1);
setTimeout(function(){
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 100; i++) {
i === 99 && resolve();
}
console.log(4);
}).then(function() {
console.log(5);
});
console.log(6);
Copy the code
Print result:
1, 3, 4, 6, 5, 2Copy the code
Try this more complicated example:
console.log(1);
setTimeout(function(){
console.log(2);
}, 10);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 10000; i++) {
i === 9999 && resolve();
}
console.log(4);
}).then(function() {
console.log(5);
});
setTimeout(function(){
console.log(7);
},1);
new Promise(function(resolve) {
console.log(8);
resolve();
}).then(function(){
console.log(9);
});
console.log(6);
Copy the code
Print result:
1
3
4
8
6
5
9
7
2
Copy the code
It is worth noting that after the completion of the micro task, the second macro task setTimeout will be executed. Since the first setTimeout is executed after 10 milliseconds, and the second setTimeout is executed after 1 millisecond, the priority of 1 millisecond is higher than that of 10 milliseconds, so finally 7 and 2 are printed respectively
Many people will find that nextTick in VUE has a higher priority than setTimeout because nextTick takes precedence over promise.then.
One of the characteristics of Vue is that it can be responsive, but when data is updated, the DOM is not immediately updated, but placed in an asynchronous queue, so if in our business scenario there is a piece of code in which the logic needs to be executed after the DOM is updated, we can use this.$nextTick() to implement this.
NextTick source code (Vue2.6.10)
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []// Store all callback functions that need to be executed
let pending = false// This variable is used to indicate the status and determine whether there is a callback being executed.
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
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
} 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)
}
// the timerFunc function will cause the data of the textNode to change because the MutationObserver is listening for the textNode.
// So the flushCallbacks callback is also triggered
isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () = > {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
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 is not a function, then the _resolve value is checked, and promise.then () is used. For example, this.$nextTick().then(cb).
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
MutationObserver
The MutationObserver is an interface that listens for DOM changes and is notified of any changes to the DOM. This property is used in Vue to listen for completion of DOM updates.
MutationObserver is asynchronous. It does not process immediately. Instead, it executes once after all the DOM on the page has been completed. The DOM will be listened for 100 times if it is an event, but our MutationObserver will only execute once, waiting for all DOM operations to complete.
MutationObserver constructor
var observer = new MutationObserver(callback);
Copy the code
The observer callback function is called after each DOM change and takes two arguments, the first being the changed array and the second being the observer’s real column.
Methods of MutationObserver instances
Observe () : This method is used to observe DOM node changes. This method takes two parameters, the first being the DOM element to observe and the second the variation type to observe.
observer.observe(dom, options);
Copy the code
Options have the following types:
- ChildList: Changes to child nodes.
- Attributes: Changes to attributes.
- CharacterData: Changes in node content or node text.
- Subtree: Changes of all descendant nodes.
Select true in the Options object to see which variation type you want to observe. But to set changes to a subtree, you must specify one or more of childList, Attributes, and characterData.