preface
Since Vue updates the DOM asynchronously, if something is done directly after a data modification, it will be executed immediately, while the DOM is not yet updated. Vue provides nextTick, which is called immediately after the data modification, to ensure that the incoming callback will be executed after the update is complete.
The next two questions explore how nextTick works
- What happens when you call nextTick?
- How does it detect that the DOM update is complete?
nextTick
Both the global API vue. nextTick and the instance method vm.$nextTick actually call the nextTick function internally. The nextTick logic is simple. All it does is push the incoming callback into the Callbacks queue, pass a resolve if there is no callback and Promise is supported, and call timerFunc if pending is false.
const callbacks = []
let pending = false
export function nextTick (cb, ctx) {
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 &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
As you can see, timerFunc is the key to deciding when to execute the callback. Pending indicates whether any asynchronous task is currently executing. If not, call timerFunc immediately and change pending to true to prevent repeated calls from blocking the process.
Calling timerFunc creates an asynchronous task, and when the asynchronous task is finished, the function in the Callbacks queue is executed, and the pending is set to false for the next nextTick to be called. When timerFunc is not finished, Repeated calls to nextTick trigger only one execution.
How does Vue implement asynchronous tasks
I can’t wait to see how timerFunc is implemented and when the callback is executed. Don’t worry, there is a bit of pre-knowledge here, which is the Event Loop of the browser
Event Loop Indicates the Event Loop
We know that JS is single-threaded, that synchronous tasks are executed sequentially, and that asynchronous tasks with event listeners for callbacks, setTimeout, Promise, and so on are not executed immediately. So how does the JS engine decide when these asynchronous tasks are invoked?
The message queue
The Javascript runtime contains a message queue that manages messages waiting to be processed. When an event listener is triggered, or a setTimeout callback is added, a message is added to the queue waiting to be processed.
Task queue
Each message has a callback function associated with it and is placed in a task queue. Each time, the message is processed from the head of the message queue and the associated callback function is executed until the callback function is completed.
Tasks and microtasks
As mentioned above, a (macro) task is associated with the message, and when the message is processed, the corresponding task is executed, such as the callback triggered by the event, and the task added using setTimeout.
Microtasks are stored independently in the microtask queue. When a (macro) task starts to execute, the newly added microtasks will be added to the microtask queue. After the task execution is completed, the microtasks will be executed successively until the microtask queue is empty before the next iteration. If you keep adding microtasks, processing will continue until the microtask queue is empty, so you want to prevent repeated addition of microtasks from blocking the process.
(macro) tasks: callback triggered by event listeners, setTimeout, setInterval microtasks: Promise, queueMicrotask, MutationObserver
The Event Loop summary
Combining the three concepts above, we can summarize these steps
- When a message is added to the message queue, its corresponding task will also be added to the task queue. When the JS engine is idle, it takes out a message to start processing, and the subsequent message will be processed only after the previous message is completed
- When the message is processed, its corresponding task is also removed from the task queue, and new microtasks added during this time, such as creating a Promise, are added to the microtask queue
- After the task is executed to completion, the microtasks will be executed successively until the microtask queue is empty
- To start processing the next message, repeat step 123, which is the event loop
TimerFunc principle
Now that you know how the event loop works, take a look at how timerFunc works and when to execute the nextTick callback function.
Can be seen in the source Vue did a lot of compatible processing in creating asynchronous tasks, in turn, trying to use Promise, MutationObserver, setImmediate, setTimeout to create asynchronous task
let timerFunc
if (typeof Promise! = ='undefined' && isNative(Promise)) {
// ...
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// ...
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
// ...
} else {
// ...
}
Copy the code
Promise.then
Then is a microtask that takes precedence over the (macro) task. A call to timerFunc will add a microtask. The flushCallbacks will be executed until DOM updates are complete and the next event loop starts
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks)
// Weird bug in iOS where microtasks are listed but not refreshed until the browser needs to handle some other work, such as timers, which are used to force the refresh queue
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
Copy the code
MutationObserver
MutationObserver receives a callback function that uses the new keyword to create and return an instance object, which is called when the specified DOM changes.
The MutationObserver, while specifying that the DOM will be triggered when it changes, only adds a microtask that will not be executed immediately, but will not be executed until all DOM updates have been made, so Vue only needs to create a node to modify its contents in this area to implement listening.
It is very clever to use counter = (counter + 1) % 2 to change counter between 0/1, call timerFunc to modify the textNode, wait for the textNode change to add a microtask, FlushCallbacks are called when DOM updates are complete
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
setImmediate
This method is used to place long-running operations in a callback that is executed as soon as the browser completes the rest of the statement.
Supported only by IE10+, is a (macro) task
timerFunc = () = > {
setImmediate(flushCallbacks)
}
Copy the code
setTimeout
If none of the above is supported, using setTimeout to create an asynchronous task is a (macro) task
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}Copy the code
flushCallbacks
As you can see above, Vue in turn tries to create an asynchronous task using Promise.then, MutationObserver, setImmediate, and setTimeout. The flushCallbacks will be executed once the DOM update is complete.
FlushCallbacks first changes pending to false, waits for the next call to timerFunc, and then iterates through the callback queue, pulling out the callbacks in turn for execution.
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
summary
Since Vue updates DOM asynchronously, nextTick needs to maintain a queue of callback functions and wait for an appropriate time to execute the callback function, which takes advantage of the event loop mechanism. When Vue updates asynchronously, a new asynchronous task is added, which will be processed after Vue updates. The callbacks are then executed in sequence.