Why does Vue have an API called nextTick?
Pre-knowledge: the browser’s event loop mechanism
First of all, we need to understand the browser’s event loop mechanism, which I summarized in this javascript event loop mechanism parsing, and this in-depth browser event loop. The main processes and responsibilities of the browser are shown below:
Among them, and Vue’snextTick
It is closely relatedJS engine threadandEvent trigger thread.
- After the first rendering of the browser page,JS engine threadandEvent trigger threadThe workflow is as follows:
- The synchronization task is executed on the JS engine (main thread), forming the execution stack
- Outside of the main thread, the event-triggering thread manages a task queue, and when an asynchronous task has a result, an event is placed in the task queue.
- When the synchronous task in the execution stack is completed, the task in the task queue is read and added to the execution stack of the main thread and the corresponding asynchronous task is executed.
In the browser environment, the common methods for creating macro tasks are:
- SetTimeout and setInterval
- Network requests, I/O
- Page interaction: DOM event callbacks, mouse, keyboard, scroll events
- Page rendering
Common ways to create micro tasks are:
- Promise.then
- MutationObserve (DOM listening)
- process.nextTick
The nextTick function uses these methods to process functions passed in with cb as asynchronous tasks.
Vue nextTick source analysis
NextTick function
- NextTick function
var callbacks = [];
var pending = false;
function nextTick(cb, ctx) { //cb? : Function, ctx? : Object
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
The function passed in with the cb argument in the nextTick function is wrapped and pushed into the Callbacks array. The variable pending is used to ensure that the timerFunc() function is executed only once in an event loop. Finally the if (! cb && typeof Promise ! == ‘undefined’), returns a Promise instance if the cb parameter does not exist and the browser supports Promise. For example, nextTick().then(() => {}), when _resolve is executed, then logic is executed.
How is the function passed in with the cb argument wrapped?
When cb has a value, cb.call(CTX) is executed in the try statement. CTX is the argument passed to the function. If the execution fails, the error is caught by a catch. If cb has no value. Execute _resolve(CTX), because a Promise instance object will be returned if cb does not exist, and execute _resolve(CTX) to execute then logic.
- TimerFunc () function
Take a look at the timerFunc() function, just the ptimerFunc function that uses Promise to create an asynchronous execution. The timerFunc function simply calls the flushCallbacks function with various asynchronously executed methods.
let timerFunc;
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks) // execute the method call flushCallbacks asynchronously
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
Copy the code
- 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
Execute pending=false to call timerFunc from nextTick in the next event loop. Execute const copies = callbacks. Slice (0) copies callbacks into constant copies and deletes callbacks. Copies are used to execute each function.
Summarize the logic of the nextTick function
A callbacks array is defined to simulate an event queue. A function passed in with the parameter cb is wrapped in a function that executes the passed function, handles the failure of execution, and the non-presence of the parameter cb, and then adds it to the Callbacks array. Call timerFunc, which calls the flushCallbacks function in a variety of asynchronously executed methods, copies each function in the callbacks, and executes it in flushCallbacks. A variable pending is defined to ensure that the timerFunc function is called only once in an event loop.
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.
Promise creates asynchronous execution functions
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
The timerFunc function calls the flushCallbacks function with various asynchronously executed methods.
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
Copy the code
Firstly, isNative method is used to determine whether browser supports Promise. Typeof Promise supports function, so this condition is met and a Promise object in resolved state is returned directly. 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.
/native code/.test(ctor.tostring ()) if Ctor is a function Check if the string after toString contains a fragment of native code. Function Promise() {[native code]} returns a Function Promise() {[native code]}.
function isNative(Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
Copy the code
The timerFunc function is called from p.chen (flushCallbacks), which executes the flushCallbacks function directly, iterating through each function passed in by nextTick. Because the Promise is a micro Task type, these functions are executed asynchronously.
MutationObserver creates asynchronous execution functions
if(! isIE &&typeofMutationObserver ! = ='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
Create and return a new MutationObserver, and pass in the flushCallbacks as a return function, which is called when the specified DOM changes and in which each nextTick passed in function is iterated to execute. Because MutationObserver is a micro Task type, these functions are executed asynchronously.
SetImmediate creates an asynchronous execution function
if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
timerFunc = () = > {
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 = () = > {
setTimeout(flushCallbacks, 0);
}
Copy the code
Compatible with Internet Explorer 10 or less, create asynchronous tasks, which are macro tasks and consume large resources.
NextTick source in vue (vue\ SRC \core\util\next-tick.js)
/* @flow */
/* globals MutationObserver */
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]()
}
}
// In version 2.5 we used a combination of microtasks and Macrotasks, but there were some minor issues with redrawing, and there were some very strange behaviors in the task queue with MacroTasks, So we went back to the previous state. Prioritize microTasks everywhere.
// Here we have async deferring wrappers using microtasks.
// In 2.5 we use (macro) tasks In combination with microtasks.
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
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 */
// Task execution priority
// Promise -> MutationObserver -> setImmediate -> setTimeout
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)
}
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()
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
Ref 1: Do you really understand $nextTick
Reference 2: Vue source code — The implementation of nextTick