- Internal Implementation of $nextTick
The function below demonstrates how Vue collects callback functions to be executed asynchronously. Instead of running them immediately, it queues them for later execution using a deferred task runner.
function scheduleTick(callback, context) {
let resolver;
queue.push(function() {
if (callback) {
try {
callback.call(context);
} catch (error) {
handleError(error, context, 'nextTick');
}
} else if (resolver) {
resolver(context);
}
});
if (!isPending) {
isPending = true;
startFlush();
}
if (!callback && typeof Promise !== 'undefined') {
return new Promise(function(resolve) {
resolver = resolve;
});
}
}
queue
Whenever scheduleTick is called multiple times, the callbacks are stored in the queue array. These are flushed all at once during the next asynchronous tick (microtask or macrotask depending on environment).
isPending
This flag ensures that the flushing mechanism (startFlush) is triggered only once per cycle. Once the queued task are processed, the flag is reset to allow future batches.
Async Strategy Selection
Vue attempts to use the most efficient asynchronous browser API available. The priority order is:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const promiseInstance = Promise.resolve();
startFlush = function() {
promiseInstance.then(runFlush);
if (isIOS) { setTimeout(noop); }
};
usesMicrotask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let count = 1;
const observer = new MutationObserver(runFlush);
const text = document.createTextNode(String(count));
observer.observe(text, { characterData: true });
startFlush = function() {
count = (count + 1) % 2;
text.data = String(count);
};
usesMicrotask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
startFlush = function() {
setImmediate(runFlush);
};
} else {
startFlush = function() {
setTimeout(runFlush, 0);
};
}
Flushing the Calllback Queue
The following function clears the pending callbacks and executes them sequentially:
function runFlush() {
isPending = false;
const copies = queue.slice(0);
queue.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
runFlush Behavior
- Resets the pending flag so that new batches can be scheduled.
- Copies and empties the queue to avoid issues if new callbacks are added during execution.
- Executes each stored callback in order.
- Why DOM Updates Require $nextTick
When reactive data changes, Vue does not update the DOM immediately. Instead, it batches updates using a watcher queue.
function enqueueWatcher(watcher) {
const id = watcher.id;
if (watcherIds[id] == null) {
watcherIds[id] = true;
if (!isFlushing) {
watcherQueue.push(watcher);
} else {
let i = watcherQueue.length - 1;
while (i > currentIndex && watcherQueue[i].id > watcher.id) {
i--;
}
watcherQueue.splice(i + 1, 0, watcher);
}
if (!isWaiting) {
isWaiting = true;
if (!config.async) {
flushQueue();
return;
}
scheduleTick(flushQueue);
}
}
}
Watcher Update Entry Point
Watcher.prototype.update = function() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
enqueueWatcher(this);
}
};
enqueueWatcher Details
- Collects watchers that need to update the view.
- Uses
scheduleTickto defer DOM updates until the next event loop iteration.
watcherIds
Ensures that the same watcher is not added multiple times to the queue during a single update cycle.
isWaiting
Prevents the update queue from being flushed more than once per cycle, even if enqueueWatcher is called multiple times.