Skip to content

Commit

Permalink
fix: async edge case fix should apply to more browsers
Browse files Browse the repository at this point in the history
also optimize fix performance by avoiding calls to performance.now()
  • Loading branch information
yyx990803 committed Jan 23, 2019
1 parent ba9907c commit ba0ebd4
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 11 deletions.
28 changes: 27 additions & 1 deletion src/core/observer/scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/core/util/next-tick.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
21 changes: 11 additions & 10 deletions src/platforms/web/runtime/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down

0 comments on commit ba0ebd4

Please sign in to comment.