Skip to content

Commit 5ee4053

Browse files
committed
fix(runtime-dom): fix event timestamp check in iframes
fix #2513 fix #3933 close #5474
1 parent a71f9ac commit 5ee4053

File tree

2 files changed

+70
-75
lines changed

2 files changed

+70
-75
lines changed

packages/runtime-dom/__tests__/patchEvents.spec.ts

+43-30
Original file line numberDiff line numberDiff line change
@@ -5,101 +5,95 @@ const timeout = () => new Promise(r => setTimeout(r))
55
describe(`runtime-dom: events patching`, () => {
66
it('should assign event handler', async () => {
77
const el = document.createElement('div')
8-
const event = new Event('click')
98
const fn = jest.fn()
109
patchProp(el, 'onClick', null, fn)
11-
el.dispatchEvent(event)
10+
el.dispatchEvent(new Event('click'))
1211
await timeout()
13-
el.dispatchEvent(event)
12+
el.dispatchEvent(new Event('click'))
1413
await timeout()
15-
el.dispatchEvent(event)
14+
el.dispatchEvent(new Event('click'))
1615
await timeout()
1716
expect(fn).toHaveBeenCalledTimes(3)
1817
})
1918

2019
it('should update event handler', async () => {
2120
const el = document.createElement('div')
22-
const event = new Event('click')
2321
const prevFn = jest.fn()
2422
const nextFn = jest.fn()
2523
patchProp(el, 'onClick', null, prevFn)
26-
el.dispatchEvent(event)
24+
el.dispatchEvent(new Event('click'))
2725
patchProp(el, 'onClick', prevFn, nextFn)
2826
await timeout()
29-
el.dispatchEvent(event)
27+
el.dispatchEvent(new Event('click'))
3028
await timeout()
31-
el.dispatchEvent(event)
29+
el.dispatchEvent(new Event('click'))
3230
await timeout()
3331
expect(prevFn).toHaveBeenCalledTimes(1)
3432
expect(nextFn).toHaveBeenCalledTimes(2)
3533
})
3634

3735
it('should support multiple event handlers', async () => {
3836
const el = document.createElement('div')
39-
const event = new Event('click')
4037
const fn1 = jest.fn()
4138
const fn2 = jest.fn()
4239
patchProp(el, 'onClick', null, [fn1, fn2])
43-
el.dispatchEvent(event)
40+
el.dispatchEvent(new Event('click'))
4441
await timeout()
4542
expect(fn1).toHaveBeenCalledTimes(1)
4643
expect(fn2).toHaveBeenCalledTimes(1)
4744
})
4845

4946
it('should unassign event handler', async () => {
5047
const el = document.createElement('div')
51-
const event = new Event('click')
5248
const fn = jest.fn()
5349
patchProp(el, 'onClick', null, fn)
5450
patchProp(el, 'onClick', fn, null)
55-
el.dispatchEvent(event)
51+
el.dispatchEvent(new Event('click'))
5652
await timeout()
5753
expect(fn).not.toHaveBeenCalled()
5854
})
5955

6056
it('should support event option modifiers', async () => {
6157
const el = document.createElement('div')
62-
const event = new Event('click')
6358
const fn = jest.fn()
6459
patchProp(el, 'onClickOnceCapture', null, fn)
65-
el.dispatchEvent(event)
60+
el.dispatchEvent(new Event('click'))
6661
await timeout()
67-
el.dispatchEvent(event)
62+
el.dispatchEvent(new Event('click'))
6863
await timeout()
6964
expect(fn).toHaveBeenCalledTimes(1)
7065
})
7166

7267
it('should unassign event handler with options', async () => {
7368
const el = document.createElement('div')
74-
const event = new Event('click')
7569
const fn = jest.fn()
7670
patchProp(el, 'onClickCapture', null, fn)
77-
el.dispatchEvent(event)
71+
el.dispatchEvent(new Event('click'))
7872
await timeout()
7973
expect(fn).toHaveBeenCalledTimes(1)
8074

8175
patchProp(el, 'onClickCapture', fn, null)
82-
el.dispatchEvent(event)
76+
el.dispatchEvent(new Event('click'))
8377
await timeout()
84-
el.dispatchEvent(event)
78+
el.dispatchEvent(new Event('click'))
8579
await timeout()
8680
expect(fn).toHaveBeenCalledTimes(1)
8781
})
8882

8983
it('should support native onclick', async () => {
9084
const el = document.createElement('div')
91-
const event = new Event('click')
9285

9386
// string should be set as attribute
9487
const fn = ((window as any).__globalSpy = jest.fn())
9588
patchProp(el, 'onclick', null, '__globalSpy(1)')
96-
el.dispatchEvent(event)
89+
el.dispatchEvent(new Event('click'))
9790
await timeout()
9891
delete (window as any).__globalSpy
9992
expect(fn).toHaveBeenCalledWith(1)
10093

10194
const fn2 = jest.fn()
10295
patchProp(el, 'onclick', '__globalSpy(1)', fn2)
96+
const event = new Event('click')
10397
el.dispatchEvent(event)
10498
await timeout()
10599
expect(fn).toHaveBeenCalledTimes(1)
@@ -108,13 +102,12 @@ describe(`runtime-dom: events patching`, () => {
108102

109103
it('should support stopImmediatePropagation on multiple listeners', async () => {
110104
const el = document.createElement('div')
111-
const event = new Event('click')
112105
const fn1 = jest.fn((e: Event) => {
113106
e.stopImmediatePropagation()
114107
})
115108
const fn2 = jest.fn()
116109
patchProp(el, 'onClick', null, [fn1, fn2])
117-
el.dispatchEvent(event)
110+
el.dispatchEvent(new Event('click'))
118111
await timeout()
119112
expect(fn1).toHaveBeenCalledTimes(1)
120113
expect(fn2).toHaveBeenCalledTimes(0)
@@ -125,35 +118,55 @@ describe(`runtime-dom: events patching`, () => {
125118
const el1 = document.createElement('div')
126119
const el2 = document.createElement('div')
127120

128-
const event = new Event('click')
121+
// const event = new Event('click')
129122
const prevFn = jest.fn()
130123
const nextFn = jest.fn()
131124

132125
patchProp(el1, 'onClick', null, prevFn)
133126
patchProp(el2, 'onClick', null, prevFn)
134127

135-
el1.dispatchEvent(event)
136-
el2.dispatchEvent(event)
128+
el1.dispatchEvent(new Event('click'))
129+
el2.dispatchEvent(new Event('click'))
137130
await timeout()
138131
expect(prevFn).toHaveBeenCalledTimes(2)
139132
expect(nextFn).toHaveBeenCalledTimes(0)
140133

141134
patchProp(el1, 'onClick', prevFn, nextFn)
142135
patchProp(el2, 'onClick', prevFn, nextFn)
143136

144-
el1.dispatchEvent(event)
145-
el2.dispatchEvent(event)
137+
el1.dispatchEvent(new Event('click'))
138+
el2.dispatchEvent(new Event('click'))
146139
await timeout()
147140
expect(prevFn).toHaveBeenCalledTimes(2)
148141
expect(nextFn).toHaveBeenCalledTimes(2)
149142

150-
el1.dispatchEvent(event)
151-
el2.dispatchEvent(event)
143+
el1.dispatchEvent(new Event('click'))
144+
el2.dispatchEvent(new Event('click'))
152145
await timeout()
153146
expect(prevFn).toHaveBeenCalledTimes(2)
154147
expect(nextFn).toHaveBeenCalledTimes(4)
155148
})
156149

150+
// vuejs/vue#6566
151+
it('should not fire handler attached by the event itself', async () => {
152+
const el = document.createElement('div')
153+
const child = document.createElement('div')
154+
el.appendChild(child)
155+
document.body.appendChild(el)
156+
const childFn = jest.fn()
157+
const parentFn = jest.fn()
158+
159+
patchProp(child, 'onClick', null, () => {
160+
childFn()
161+
patchProp(el, 'onClick', null, parentFn)
162+
})
163+
child.dispatchEvent(new Event('click', { bubbles: true }))
164+
165+
await timeout()
166+
expect(childFn).toHaveBeenCalled()
167+
expect(parentFn).not.toHaveBeenCalled()
168+
})
169+
157170
// #2841
158171
test('should patch event correctly in web-components', async () => {
159172
class TestElement extends HTMLElement {

packages/runtime-dom/src/modules/events.ts

+27-45
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,6 @@ interface Invoker extends EventListener {
1212

1313
type EventValue = Function | Function[]
1414

15-
// Async edge case fix requires storing an event listener's attach timestamp.
16-
const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
17-
let _getNow = Date.now
18-
let skipTimestampCheck = false
19-
if (typeof window !== 'undefined') {
20-
// Determine what event timestamp the browser is using. Annoyingly, the
21-
// timestamp can either be hi-res (relative to page load) or low-res
22-
// (relative to UNIX epoch), so in order to compare time we have to use the
23-
// same timestamp type when saving the flush timestamp.
24-
if (Date.now() > document.createEvent('Event').timeStamp) {
25-
// if the low-res timestamp which is bigger than the event timestamp
26-
// (which is evaluated AFTER) it means the event is using a hi-res timestamp,
27-
// and we need to use the hi-res version for event listeners as well.
28-
_getNow = performance.now.bind(performance)
29-
}
30-
// #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
31-
// and does not fire microtasks in between event propagation, so safe to exclude.
32-
const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i)
33-
skipTimestampCheck = !!(ffMatch && Number(ffMatch[1]) <= 53)
34-
}
35-
return [_getNow, skipTimestampCheck]
36-
})()
37-
38-
// To avoid the overhead of repeatedly calling performance.now(), we cache
39-
// and use the same timestamp for all event listeners attached in the same tick.
40-
let cachedNow: number = 0
41-
const p = /*#__PURE__*/ Promise.resolve()
42-
const reset = () => {
43-
cachedNow = 0
44-
}
45-
const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
46-
4715
export function addEventListener(
4816
el: Element,
4917
event: string,
@@ -105,27 +73,41 @@ function parseName(name: string): [string, EventListenerOptions | undefined] {
10573
return [event, options]
10674
}
10775

76+
// To avoid the overhead of repeatedly calling Date.now(), we cache
77+
// and use the same timestamp for all event listeners attached in the same tick.
78+
let cachedNow: number = 0
79+
const p = /*#__PURE__*/ Promise.resolve()
80+
const getNow = () =>
81+
cachedNow || (p.then(() => (cachedNow = 0)), (cachedNow = Date.now()))
82+
10883
function createInvoker(
10984
initialValue: EventValue,
11085
instance: ComponentInternalInstance | null
11186
) {
112-
const invoker: Invoker = (e: Event) => {
113-
// async edge case #6566: inner click event triggers patch, event handler
87+
const invoker: Invoker = (e: Event & { _vts?: number }) => {
88+
// async edge case vuejs/vue#6566
89+
// inner click event triggers patch, event handler
11490
// attached to outer element during patch, and triggered again. This
11591
// happens because browsers fire microtask ticks between event propagation.
116-
// the solution is simple: we save the timestamp when a handler is attached,
117-
// and the handler would only fire if the event passed to it was fired
92+
// this no longer happens for templates in Vue 3, but could still be
93+
// theoretically possible for hand-written render functions.
94+
// the solution: we save the timestamp when a handler is attached,
95+
// and also attach the timestamp to any event that was handled by vue
96+
// for the first time (to avoid inconsistent event timestamp implementations
97+
// or events fired from iframes, e.g. #2513)
98+
// The handler would only fire if the event passed to it was fired
11899
// AFTER it was attached.
119-
const timeStamp = e.timeStamp || _getNow()
120-
121-
if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
122-
callWithAsyncErrorHandling(
123-
patchStopImmediatePropagation(e, invoker.value),
124-
instance,
125-
ErrorCodes.NATIVE_EVENT_HANDLER,
126-
[e]
127-
)
100+
if (!e._vts) {
101+
e._vts = Date.now()
102+
} else if (e._vts <= invoker.attached) {
103+
return
128104
}
105+
callWithAsyncErrorHandling(
106+
patchStopImmediatePropagation(e, invoker.value),
107+
instance,
108+
ErrorCodes.NATIVE_EVENT_HANDLER,
109+
[e]
110+
)
129111
}
130112
invoker.value = initialValue
131113
invoker.attached = getNow()

0 commit comments

Comments
 (0)