Collected · Vue · MIT
Temporary exhibition · Hall of the Commons
Four Ways to Wait
JavaScript·2019·110 lines·3673 bytes
/* @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]()
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (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 */
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 && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. 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 (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically 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
})
}
}Curator’s note
Every framework needs somewhere to put work that must happen soon but not now. This is Vue's. Change some data, and the DOM does not update on the next line — it updates on the next tick, and this file decides when that is.
Most of it is not code. Forty lines do the job; the rest is the record of finding out what the job was. timerFunc is chosen once, at load, by trying four things in order: native Promise, then MutationObserver, then setImmediate, then setTimeout. That is a walk down every mechanism JavaScript ever offered for deferring work, in the order they arrived, and the comments say who broke each one. UIWebView on iOS 9.3.3, where Promise.then gets stuck in "a weird state." PhantomJS. iOS 7. Android 4.4. IE11, where MutationObserver is unreliable. Eleven issue numbers are cited inline, as evidence.
Two lines are worth standing in front of. The first is if (isIOS) setTimeout(noop) — an empty timer, scheduled for no reason except to make a browser notice that it has stopped flushing its own microtask queue. A workaround for a workaround, four words long. The second is the comment above timerFunc, which records a decision being reversed: 2.5 used macrotasks, and 2.6 went back to microtasks everywhere, "again". The paragraph then sets out what that cost, with issue numbers, rather than claiming it was free.
The file also exports isUsingMicroTask — a confession about which of the four it settled on, so that code elsewhere in the framework can compensate for the answer.
Almost everything in this museum shows a good idea. This shows the other half of the work: what it costs to make a good idea survive contact with browsers that are wrong. It is finished, too. Vue 2 was retired at the end of 2023, and apart from being translated into TypeScript in 2021 this file had not changed in four years. Nothing more will be added to it.
Related: why a queue is not the same as doing two things at once, and what it costs to change a decision after people depend on it.