Skip to content

Commit ba0ebd4

Browse files
committed
fix: async edge case fix should apply to more browsers
also optimize fix performance by avoiding calls to performance.now()
1 parent ba9907c commit ba0ebd4

File tree

3 files changed

+42
-11
lines changed

3 files changed

+42
-11
lines changed

src/core/observer/scheduler.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { callHook, activateChildComponent } from '../instance/lifecycle'
77
import {
88
warn,
99
nextTick,
10-
devtools
10+
devtools,
11+
inBrowser
1112
} from '../util/index'
1213

1314
export const MAX_UPDATE_COUNT = 100
@@ -32,10 +33,35 @@ function resetSchedulerState () {
3233
waiting = flushing = false
3334
}
3435

36+
// Async edge case #6566 requires saving the timestamp when event listeners are
37+
// attached. However, calling performance.now() has a perf overhead especially
38+
// if the page has thousands of event listeners. Instead, we take a timestamp
39+
// every time the scheduler flushes and use that for all event listeners
40+
// attached during that flush.
41+
export let currentFlushTimestamp = 0
42+
43+
let getNow
44+
if (inBrowser) {
45+
// Determine what event timestamp the browser is using. Annoyingly, the
46+
// timestamp can either be hi-res ( relative to poge load) or low-res
47+
// (relative to UNIX epoch), so in order to compare time we have to use the
48+
// same timestamp type when saving the flush timestamp.
49+
const lowResNow = Date.now()
50+
const eventTimestamp = document.createEvent('Event').timeStamp
51+
// the event timestamp is created after Date.now(), if it's smaller
52+
// it means it's using a hi-res timestamp.
53+
getNow = eventTimestamp < lowResNow
54+
? () => performance.now() // hi-res
55+
: Date.now // low-res
56+
} else {
57+
getNow = Date.now
58+
}
59+
3560
/**
3661
* Flush both queues and run the watchers.
3762
*/
3863
function flushSchedulerQueue () {
64+
currentFlushTimestamp = getNow()
3965
flushing = true
4066
let watcher, id
4167

src/core/util/next-tick.js

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { noop } from 'shared/util'
55
import { handleError } from './error'
66
import { isIE, isIOS, isNative } from './env'
77

8+
export let isUsingMicroTask = false
9+
810
const callbacks = []
911
let pending = false
1012

@@ -48,6 +50,7 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) {
4850
// "force" the microtask queue to be flushed by adding an empty timer.
4951
if (isIOS) setTimeout(noop)
5052
}
53+
isUsingMicroTask = true
5154
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
5255
isNative(MutationObserver) ||
5356
// PhantomJS and iOS 7.x
@@ -66,6 +69,7 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) {
6669
counter = (counter + 1) % 2
6770
textNode.data = String(counter)
6871
}
72+
isUsingMicroTask = true
6973
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
7074
// Fallback to setImmediate.
7175
// Techinically it leverages the (macro) task queue,

src/platforms/web/runtime/modules/events.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { isDef, isUndef } from 'shared/util'
44
import { updateListeners } from 'core/vdom/helpers/index'
5-
import { isIE, isChrome, supportsPassive } from 'core/util/index'
5+
import { isIE, supportsPassive, isUsingMicroTask } from 'core/util/index'
66
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
7+
import { currentFlushTimestamp } from 'core/observer/scheduler'
78

89
// normalize v-model event tokens that can only be determined at runtime.
910
// it's important to place the event as the first in the array because
@@ -44,17 +45,17 @@ function add (
4445
capture: boolean,
4546
passive: boolean
4647
) {
47-
if (isChrome) {
48-
// async edge case #6566: inner click event triggers patch, event handler
49-
// attached to outer element during patch, and triggered again. This only
50-
// happens in Chrome as it fires microtask ticks between event propagation.
51-
// the solution is simple: we save the timestamp when a handler is attached,
52-
// and the handler would only fire if the event passed to it was fired
53-
// AFTER it was attached.
54-
const now = performance.now()
48+
// async edge case #6566: inner click event triggers patch, event handler
49+
// attached to outer element during patch, and triggered again. This only
50+
// happens in Chrome as it fires microtask ticks between event propagation.
51+
// the solution is simple: we save the timestamp when a handler is attached,
52+
// and the handler would only fire if the event passed to it was fired
53+
// AFTER it was attached.
54+
if (isUsingMicroTask) {
55+
const attachedTimestamp = currentFlushTimestamp
5556
const original = handler
5657
handler = original._wrapper = function (e) {
57-
if (e.timeStamp >= now) {
58+
if (e.timeStamp >= attachedTimestamp) {
5859
return original.apply(this, arguments)
5960
}
6061
}

0 commit comments

Comments
 (0)