Understanding Vue.nextTick and the JavaScript Event Loop
Vue.nextTick schedules callbacks using a graceful‑degradation chain (Promise, MutationObserver, setImmediate, setTimeout) that leverages the JavaScript event‑loop’s micro‑task queue, ensuring DOM mutations are applied instantly but rendered after microtasks and before macrotasks, while the browser’s multi‑threaded architecture coordinates rendering, timers, and network work.
Background
The author noticed confusion around Vue's nextTick() method, which can lead to misuse of API code. The official Vue documentation states that Vue.nextTick() executes a delayed callback after the next DOM update cycle finishes.
Vue.nextTick Source Exploration
The core of next-tick.js defines four variables: callbacks , flushCallbacks , timerFunc , and pending . The most critical part is timerFunc , which selects the appropriate mechanism to trigger flushCallbacks based on the environment.
let timerFunc
// 1. Prefer native Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic WebViews add an empty timer to force micro‑task queue flush
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
// 2. Fallback to MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
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
}
// 3. Use setImmediate if available
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
// 4. Final fallback to setTimeout
else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}This code shows a graceful degradation strategy: it first tries to use a Promise (micro‑task), then MutationObserver, then setImmediate , and finally setTimeout .
Event Loop Mechanism
JavaScript runtime separates tasks into macrotasks and microtasks. Macrotasks are triggered by the host environment (e.g., setTimeout , I/O, UI events). Microtasks are native to the language (e.g., Promise.then , MutationObserver ). The execution order is:
When the call stack runs, any encountered micro‑ or macrotasks are queued.
After the stack empties, the engine processes the microtask queue (FIFO).
When the microtask queue is empty, the engine processes one macrotask.
The cycle repeats until both queues are empty.
The author provides a diagram (omitted) illustrating this flow.
DOM Rendering Timing in the Event Loop
Through experiments, the author demonstrates that DOM changes become effective immediately, but the actual rendering occurs after microtasks and before macrotasks.
document.body.style.background = 'blue';
console.log(1);
setTimeout(() => {
document.body.style.background = 'yellow';
console.log(2);
}, 0);
Promise.resolve().then(() => {
document.body.style.background = 'red';
console.log(3);
});
console.log(4);Output order: 1, 4, 3, 2 . The page changes from red to yellow, never showing the intermediate blue, confirming that rendering happens after the microtask ( Promise.then ) and before the macrotask ( setTimeout ).
document.body.style.background = 'blue';
console.log(1, document.body.style.background);
Promise.resolve().then(() => {
document.body.style.background = 'red';
console.log(2, document.body.style.background);
});
setTimeout(() => {
document.body.style.background = 'yellow';
console.log(3, document.body.style.background);
}, 0);
console.log(4, document.body.style.background);The logs show that the DOM style value updates instantly, while the visual rendering is deferred.
Thread Cooperation in the Browser
The article references Li Bing's “Browser Working Principles and Practice” to explain that a modern browser runs multiple processes: a main process, GPU process, network process, several renderer processes, etc. Within each renderer process, there are distinct threads:
JavaScript engine thread – executes JS code and manipulates the DOM.
GUI rendering thread – paints the UI; it is mutually exclusive with the JS thread.
Event dispatch thread – queues events (timers, AJAX, user interactions) for the JS thread.
Timer thread – handles setTimeout / setInterval timing.
Asynchronous HTTP thread – performs network requests and posts callbacks as macrotasks.
The event loop coordinates these threads, allowing the single‑threaded JS engine to handle asynchronous work.
Summary
Vue.nextTick uses a graceful degradation strategy based on the event‑loop mechanism to schedule callbacks.
DOM mutations take effect instantly, but rendering is delayed until after microtasks and before macrotasks, which improves performance by batching renders.
The browser’s multi‑threaded architecture (JS engine, GUI thread, event thread, timer thread, network thread) works together through the event loop.
Best Practices
1. When using Vue’s two‑way binding, read the updated DOM only inside a nextTick callback.
// Vue API usage example
// Modify data
vm.msg = 'Hello';
// DOM has not updated yet
Vue.nextTick(function () {
// DOM is now updated
});2. For non‑Vue DOM updates, you can access the DOM immediately after the update statement.
3. Batch multiple DOM manipulations within the same event‑loop turn (either in the current call stack or a microtask) to trigger a single render and avoid unnecessary repaints.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.