NextTick in nextTick Vue involves the asynchronous updating of the DOM in Vue. Among them, the source code of nextTick involves a lot of knowledge. NextTick is a core implementation of Vue. Before introducing The nextTick of Vue, in order to facilitate everyone’s understanding, I first briefly introduce the running mechanism of JS.
JS runtime mechanism
JS execution is single threaded and is based on event loops. The event cycle can be roughly divided into the following steps:
(1) All synchronization tasks are executed on the main thread, forming an execution context stack.
(2) In addition to the main thread, there is a task queue. Whenever an asynchronous task has a result, an event is placed in the “task queue”.
(3) Once all synchronization tasks in the “execution stack” are completed, the system reads the “task queue” to see what events are in it. Those corresponding asynchronous tasks then end the wait state, enter the execution stack, and start executing.
(4) The main thread repeats step 3 above.
The execution of the main thread is a tick, and all asynchronous results are scheduled through the “task queue”. Message queues hold individual tasks. According to the specification, tasks fall into two categories, macro task and Micro Task. After each Macro task is finished, all micro tasks should be cleared.
Macro Task and Micro Task: Macro Task and Micro Task
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask ofmicroTaskQueue) { handleMicroTask(microTask); }}Copy the code
In the browser environment, common Macro tasks include setTimeout, MessageChannel, postMessage, and setImmediate. Common micro Tasks are MutationObsever and Promise.then.
Vue implementation (Vue source 2.5+)
After Vue source 2.5+, nextTick’s implementation has a separate JS file to maintain it, and it’s not much source code, just over 100 lines in all. SRC /core/util/next-tick.js: SRC /core/util/next-tick.js
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
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]()
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we use microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
macroTimerFunc = (a)= > {
setImmediate(flushCallbacks)
}
} else if (typeofMessageChannel ! = ='undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = (a)= > {
port.postMessage(1)}}else {
/* istanbul ignore next */
macroTimerFunc = (a)= > {
setTimeout(flushCallbacks, 0)}}// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = (a)= > {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function) :Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null.arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb? : Function, ctx? : Object) {
let _resolve
callbacks.push((a)= > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
if(! pending) { pending =true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
Next-tick. js declares two variables microTimerFunc and macroTimerFunc, which correspond to micro task functions and Macro task functions respectively. With macro Task implementations, native setImmediate is a feature supported only by advanced VERSIONS of IE and Edge, and native MessageChannel is tested first. If it is not supported, it is degraded to setTimeout 0; For micro Task implementations, we check if the browser natively supports Promise, and point directly to macro Task implementations if it doesn’t.
NextTick (flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ The logic is very simple. It presses the cb callback into the callbacks array and finally executes macroTimerFunc or microTimerFunc according to the useMacroTask condition. They all execute flushCallbacks on the next tick. The logic for flushCallbacks is very simple: iterate through the callbacks and execute the corresponding callback function.
The reason for using callbacks instead of executing callbacks directly on nextTick is to ensure that executing nextTick multiple times does not start multiple asynchronous tasks, but instead pushes them into a single synchronous task that will be executed on the nextTick.
The nextTick function ends with a bit of logic:
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
Copy the code
This provides a promise-like call when nextTick does not pass cb arguments, such as:
nextTick().then((a)= > {})
Copy the code
When the _resolve function executes, it jumps to the logic of then.
Next-tick. js also exposes the withMacroTask function, which wraps the function to ensure arbitrary data modification during function execution, triggering changes and forcing macroTimerFunc to be removed when nextTick is executed. For example, for some DOM interaction events, such as v-ON-bound event callback functions, macro Task will be forced.
Vue.js provides two ways to call nextTick: the global API vue.nexttick and the instance method vm.$nextTick. No matter which one we use, You end up calling the nextTick method implemented in next-tick.js.
Take a look at the source code before VUe2.5
/** * Defer a task to execute it asynchronously. */
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 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 if */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err= > { console.error(err) }
timerFunc = (a)= > {
p.then(nextTickHandler).catch(logError)
// 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)
}
} 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
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = (a)= > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = (a)= > {
setTimeout(nextTickHandler, 0)}}return function queueNextTick (cb? : Function, ctx? : Object) {
let _resolve
callbacks.push((a)= > {
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, reject) = > {
_resolve = resolve
})
}
}
})()
Copy the code
First, take a look at the three important variables defined in nextTick.
callbacks
Store all callbacks that need to be executed
pending
Used to indicate whether a callback function is being executed
timerFunc
Next, take a look at the nextTickHandler() function.
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
This function is used to execute all the callbacks stored in the Callbacks. Next, assign the trigger mode to timerFunc.
Determine if promise is supported natively, and if so, use the promise to trigger the execution of the callback function. Otherwise, if MutationObserver is supported, an observer object is instantiated and all callbacks are triggered when the text node changes. If neither is supported, setTimeout is used to set the delay to 0.
Finally, the queueNextTick function. Since nextTick is an immediate function, queueNextTick is the function that returns, accepts arguments passed in by the user, and stores the callback function in the callbacks.
Above is the whole process. The key is timeFunc(), which delays execution. From the above, you can see that there are three implementations of timeFunc().
Promise
MutationObserver
setTimeout
Promise and setTimeout are well understood as asynchronous tasks that call back specific functions after synchronization and asynchronous tasks that update the DOM. Here is a brief introduction to MutationObserver. MutationObserver is a new API in HTML5 that monitors DOM changes. It can listen for child node deletions, property changes, text content changes, and so on on a DOM object. The call is simple, but a little unusual: you need to bind the callback first:
var mo = new MutationObserver(callback)
Copy the code
Copy code gets a MutationObserver instance by passing a callback to the constructor of MutationObserver, which is triggered when the MutationObserver instance listens for changes. At this point you are just binding the callback to the MutationObserver instance. It is not set which DOM it listens for, node deletion, or property modification. This is accomplished by calling his Observer method:
varDomTarget = the DOM node you want to listen on mo.observe(domTarget, {characterData: true // Listen for text content changes.
})
Copy the code
The role of MutationObserver in nextTick is shown in the figure above. After listening for DOM updates, the callback function is called. The reason for using MutationObserver was that nextTick wanted an asynchronous API to execute the asynchronous callbacks I wanted to execute, including Promise and setTimeout, after the current synchronous code had finished executing. It also involves microtask in depth.
conclusion
$nextTick was using setImmediate > MessageChannel > setTimeout
$nextTick was using Promise > MutationObserver > setTimeout
Through the analysis of nextTick in this section, combined with setter analysis in the previous section, we learned that the change of data to DOM re-rendering is an asynchronous process that occurs in the nextTick. This is what happens during development, such as when we get data from the server interface, and the data is modified. If some of our methods rely on DOM changes after the data modification, we have to execute it after nextTick. For example, the following pseudocode:
getData(res).then((a)= >{
this.xxx = res.data
this.$nextTick((a)= > {
// Here we can get the changed DOM})})Copy the code
reference
Vue. Js technology revealed
Briefly understand nextTick in Vue