fix: async edge case fix should apply to more browsers

also optimize fix performance by avoiding calls to performance.now()
This commit is contained in:
Evan You 2019-01-23 18:41:50 -05:00
parent ba9907c73c
commit ba0ebd4771
3 changed files with 42 additions and 11 deletions

View File

@ -7,7 +7,8 @@ import { callHook, activateChildComponent } from '../instance/lifecycle'
import {
warn,
nextTick,
devtools
devtools,
inBrowser
} from '../util/index'
export const MAX_UPDATE_COUNT = 100
@ -32,10 +33,35 @@ function resetSchedulerState () {
waiting = flushing = false
}
// Async edge case #6566 requires saving the timestamp when event listeners are
// attached. However, calling performance.now() has a perf overhead especially
// if the page has thousands of event listeners. Instead, we take a timestamp
// every time the scheduler flushes and use that for all event listeners
// attached during that flush.
export let currentFlushTimestamp = 0
let getNow
if (inBrowser) {
// Determine what event timestamp the browser is using. Annoyingly, the
// timestamp can either be hi-res ( relative to poge load) or low-res
// (relative to UNIX epoch), so in order to compare time we have to use the
// same timestamp type when saving the flush timestamp.
const lowResNow = Date.now()
const eventTimestamp = document.createEvent('Event').timeStamp
// the event timestamp is created after Date.now(), if it's smaller
// it means it's using a hi-res timestamp.
getNow = eventTimestamp < lowResNow
? () => performance.now() // hi-res
: Date.now // low-res
} else {
getNow = Date.now
}
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id

View File

@ -5,6 +5,8 @@ import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
@ -48,6 +50,7 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) {
// "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
@ -66,6 +69,7 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,

View File

@ -2,8 +2,9 @@
import { isDef, isUndef } from 'shared/util'
import { updateListeners } from 'core/vdom/helpers/index'
import { isIE, isChrome, supportsPassive } from 'core/util/index'
import { isIE, supportsPassive, isUsingMicroTask } from 'core/util/index'
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
import { currentFlushTimestamp } from 'core/observer/scheduler'
// normalize v-model event tokens that can only be determined at runtime.
// it's important to place the event as the first in the array because
@ -44,17 +45,17 @@ function add (
capture: boolean,
passive: boolean
) {
if (isChrome) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This only
// happens in Chrome as it fires microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
const now = performance.now()
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This only
// happens in Chrome as it fires microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (isUsingMicroTask) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (e.timeStamp >= now) {
if (e.timeStamp >= attachedTimestamp) {
return original.apply(this, arguments)
}
}