mirror of https://github.com/vuejs/core.git
fix(runtime-dom): fix event timestamp check in iframes
fix #2513 fix #3933 close #5474
This commit is contained in:
parent
a71f9ac41a
commit
5ee40532a6
|
@ -5,30 +5,28 @@ const timeout = () => new Promise(r => setTimeout(r))
|
||||||
describe(`runtime-dom: events patching`, () => {
|
describe(`runtime-dom: events patching`, () => {
|
||||||
it('should assign event handler', async () => {
|
it('should assign event handler', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
patchProp(el, 'onClick', null, fn)
|
patchProp(el, 'onClick', null, fn)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).toHaveBeenCalledTimes(3)
|
expect(fn).toHaveBeenCalledTimes(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update event handler', async () => {
|
it('should update event handler', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const prevFn = jest.fn()
|
const prevFn = jest.fn()
|
||||||
const nextFn = jest.fn()
|
const nextFn = jest.fn()
|
||||||
patchProp(el, 'onClick', null, prevFn)
|
patchProp(el, 'onClick', null, prevFn)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
patchProp(el, 'onClick', prevFn, nextFn)
|
patchProp(el, 'onClick', prevFn, nextFn)
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(prevFn).toHaveBeenCalledTimes(1)
|
expect(prevFn).toHaveBeenCalledTimes(1)
|
||||||
expect(nextFn).toHaveBeenCalledTimes(2)
|
expect(nextFn).toHaveBeenCalledTimes(2)
|
||||||
|
@ -36,11 +34,10 @@ describe(`runtime-dom: events patching`, () => {
|
||||||
|
|
||||||
it('should support multiple event handlers', async () => {
|
it('should support multiple event handlers', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn1 = jest.fn()
|
const fn1 = jest.fn()
|
||||||
const fn2 = jest.fn()
|
const fn2 = jest.fn()
|
||||||
patchProp(el, 'onClick', null, [fn1, fn2])
|
patchProp(el, 'onClick', null, [fn1, fn2])
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn1).toHaveBeenCalledTimes(1)
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
expect(fn2).toHaveBeenCalledTimes(1)
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
@ -48,58 +45,55 @@ describe(`runtime-dom: events patching`, () => {
|
||||||
|
|
||||||
it('should unassign event handler', async () => {
|
it('should unassign event handler', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
patchProp(el, 'onClick', null, fn)
|
patchProp(el, 'onClick', null, fn)
|
||||||
patchProp(el, 'onClick', fn, null)
|
patchProp(el, 'onClick', fn, null)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).not.toHaveBeenCalled()
|
expect(fn).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support event option modifiers', async () => {
|
it('should support event option modifiers', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
patchProp(el, 'onClickOnceCapture', null, fn)
|
patchProp(el, 'onClickOnceCapture', null, fn)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should unassign event handler with options', async () => {
|
it('should unassign event handler with options', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
patchProp(el, 'onClickCapture', null, fn)
|
patchProp(el, 'onClickCapture', null, fn)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
patchProp(el, 'onClickCapture', fn, null)
|
patchProp(el, 'onClickCapture', fn, null)
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support native onclick', async () => {
|
it('should support native onclick', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
|
|
||||||
// string should be set as attribute
|
// string should be set as attribute
|
||||||
const fn = ((window as any).__globalSpy = jest.fn())
|
const fn = ((window as any).__globalSpy = jest.fn())
|
||||||
patchProp(el, 'onclick', null, '__globalSpy(1)')
|
patchProp(el, 'onclick', null, '__globalSpy(1)')
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
delete (window as any).__globalSpy
|
delete (window as any).__globalSpy
|
||||||
expect(fn).toHaveBeenCalledWith(1)
|
expect(fn).toHaveBeenCalledWith(1)
|
||||||
|
|
||||||
const fn2 = jest.fn()
|
const fn2 = jest.fn()
|
||||||
patchProp(el, 'onclick', '__globalSpy(1)', fn2)
|
patchProp(el, 'onclick', '__globalSpy(1)', fn2)
|
||||||
|
const event = new Event('click')
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(event)
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
@ -108,13 +102,12 @@ describe(`runtime-dom: events patching`, () => {
|
||||||
|
|
||||||
it('should support stopImmediatePropagation on multiple listeners', async () => {
|
it('should support stopImmediatePropagation on multiple listeners', async () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
const event = new Event('click')
|
|
||||||
const fn1 = jest.fn((e: Event) => {
|
const fn1 = jest.fn((e: Event) => {
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
})
|
})
|
||||||
const fn2 = jest.fn()
|
const fn2 = jest.fn()
|
||||||
patchProp(el, 'onClick', null, [fn1, fn2])
|
patchProp(el, 'onClick', null, [fn1, fn2])
|
||||||
el.dispatchEvent(event)
|
el.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(fn1).toHaveBeenCalledTimes(1)
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
expect(fn2).toHaveBeenCalledTimes(0)
|
expect(fn2).toHaveBeenCalledTimes(0)
|
||||||
|
@ -125,15 +118,15 @@ describe(`runtime-dom: events patching`, () => {
|
||||||
const el1 = document.createElement('div')
|
const el1 = document.createElement('div')
|
||||||
const el2 = document.createElement('div')
|
const el2 = document.createElement('div')
|
||||||
|
|
||||||
const event = new Event('click')
|
// const event = new Event('click')
|
||||||
const prevFn = jest.fn()
|
const prevFn = jest.fn()
|
||||||
const nextFn = jest.fn()
|
const nextFn = jest.fn()
|
||||||
|
|
||||||
patchProp(el1, 'onClick', null, prevFn)
|
patchProp(el1, 'onClick', null, prevFn)
|
||||||
patchProp(el2, 'onClick', null, prevFn)
|
patchProp(el2, 'onClick', null, prevFn)
|
||||||
|
|
||||||
el1.dispatchEvent(event)
|
el1.dispatchEvent(new Event('click'))
|
||||||
el2.dispatchEvent(event)
|
el2.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(prevFn).toHaveBeenCalledTimes(2)
|
expect(prevFn).toHaveBeenCalledTimes(2)
|
||||||
expect(nextFn).toHaveBeenCalledTimes(0)
|
expect(nextFn).toHaveBeenCalledTimes(0)
|
||||||
|
@ -141,19 +134,39 @@ describe(`runtime-dom: events patching`, () => {
|
||||||
patchProp(el1, 'onClick', prevFn, nextFn)
|
patchProp(el1, 'onClick', prevFn, nextFn)
|
||||||
patchProp(el2, 'onClick', prevFn, nextFn)
|
patchProp(el2, 'onClick', prevFn, nextFn)
|
||||||
|
|
||||||
el1.dispatchEvent(event)
|
el1.dispatchEvent(new Event('click'))
|
||||||
el2.dispatchEvent(event)
|
el2.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(prevFn).toHaveBeenCalledTimes(2)
|
expect(prevFn).toHaveBeenCalledTimes(2)
|
||||||
expect(nextFn).toHaveBeenCalledTimes(2)
|
expect(nextFn).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
el1.dispatchEvent(event)
|
el1.dispatchEvent(new Event('click'))
|
||||||
el2.dispatchEvent(event)
|
el2.dispatchEvent(new Event('click'))
|
||||||
await timeout()
|
await timeout()
|
||||||
expect(prevFn).toHaveBeenCalledTimes(2)
|
expect(prevFn).toHaveBeenCalledTimes(2)
|
||||||
expect(nextFn).toHaveBeenCalledTimes(4)
|
expect(nextFn).toHaveBeenCalledTimes(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// vuejs/vue#6566
|
||||||
|
it('should not fire handler attached by the event itself', async () => {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
const child = document.createElement('div')
|
||||||
|
el.appendChild(child)
|
||||||
|
document.body.appendChild(el)
|
||||||
|
const childFn = jest.fn()
|
||||||
|
const parentFn = jest.fn()
|
||||||
|
|
||||||
|
patchProp(child, 'onClick', null, () => {
|
||||||
|
childFn()
|
||||||
|
patchProp(el, 'onClick', null, parentFn)
|
||||||
|
})
|
||||||
|
child.dispatchEvent(new Event('click', { bubbles: true }))
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
expect(childFn).toHaveBeenCalled()
|
||||||
|
expect(parentFn).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
// #2841
|
// #2841
|
||||||
test('should patch event correctly in web-components', async () => {
|
test('should patch event correctly in web-components', async () => {
|
||||||
class TestElement extends HTMLElement {
|
class TestElement extends HTMLElement {
|
||||||
|
|
|
@ -12,38 +12,6 @@ interface Invoker extends EventListener {
|
||||||
|
|
||||||
type EventValue = Function | Function[]
|
type EventValue = Function | Function[]
|
||||||
|
|
||||||
// Async edge case fix requires storing an event listener's attach timestamp.
|
|
||||||
const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
|
|
||||||
let _getNow = Date.now
|
|
||||||
let skipTimestampCheck = false
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Determine what event timestamp the browser is using. Annoyingly, the
|
|
||||||
// timestamp can either be hi-res (relative to page 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.
|
|
||||||
if (Date.now() > document.createEvent('Event').timeStamp) {
|
|
||||||
// if the low-res timestamp which is bigger than the event timestamp
|
|
||||||
// (which is evaluated AFTER) it means the event is using a hi-res timestamp,
|
|
||||||
// and we need to use the hi-res version for event listeners as well.
|
|
||||||
_getNow = performance.now.bind(performance)
|
|
||||||
}
|
|
||||||
// #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
|
|
||||||
// and does not fire microtasks in between event propagation, so safe to exclude.
|
|
||||||
const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i)
|
|
||||||
skipTimestampCheck = !!(ffMatch && Number(ffMatch[1]) <= 53)
|
|
||||||
}
|
|
||||||
return [_getNow, skipTimestampCheck]
|
|
||||||
})()
|
|
||||||
|
|
||||||
// To avoid the overhead of repeatedly calling performance.now(), we cache
|
|
||||||
// and use the same timestamp for all event listeners attached in the same tick.
|
|
||||||
let cachedNow: number = 0
|
|
||||||
const p = /*#__PURE__*/ Promise.resolve()
|
|
||||||
const reset = () => {
|
|
||||||
cachedNow = 0
|
|
||||||
}
|
|
||||||
const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
|
|
||||||
|
|
||||||
export function addEventListener(
|
export function addEventListener(
|
||||||
el: Element,
|
el: Element,
|
||||||
event: string,
|
event: string,
|
||||||
|
@ -105,27 +73,41 @@ function parseName(name: string): [string, EventListenerOptions | undefined] {
|
||||||
return [event, options]
|
return [event, options]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To avoid the overhead of repeatedly calling Date.now(), we cache
|
||||||
|
// and use the same timestamp for all event listeners attached in the same tick.
|
||||||
|
let cachedNow: number = 0
|
||||||
|
const p = /*#__PURE__*/ Promise.resolve()
|
||||||
|
const getNow = () =>
|
||||||
|
cachedNow || (p.then(() => (cachedNow = 0)), (cachedNow = Date.now()))
|
||||||
|
|
||||||
function createInvoker(
|
function createInvoker(
|
||||||
initialValue: EventValue,
|
initialValue: EventValue,
|
||||||
instance: ComponentInternalInstance | null
|
instance: ComponentInternalInstance | null
|
||||||
) {
|
) {
|
||||||
const invoker: Invoker = (e: Event) => {
|
const invoker: Invoker = (e: Event & { _vts?: number }) => {
|
||||||
// async edge case #6566: inner click event triggers patch, event handler
|
// async edge case vuejs/vue#6566
|
||||||
|
// inner click event triggers patch, event handler
|
||||||
// attached to outer element during patch, and triggered again. This
|
// attached to outer element during patch, and triggered again. This
|
||||||
// happens because browsers fire microtask ticks between event propagation.
|
// happens because browsers fire microtask ticks between event propagation.
|
||||||
// the solution is simple: we save the timestamp when a handler is attached,
|
// this no longer happens for templates in Vue 3, but could still be
|
||||||
// and the handler would only fire if the event passed to it was fired
|
// theoretically possible for hand-written render functions.
|
||||||
|
// the solution: we save the timestamp when a handler is attached,
|
||||||
|
// and also attach the timestamp to any event that was handled by vue
|
||||||
|
// for the first time (to avoid inconsistent event timestamp implementations
|
||||||
|
// or events fired from iframes, e.g. #2513)
|
||||||
|
// The handler would only fire if the event passed to it was fired
|
||||||
// AFTER it was attached.
|
// AFTER it was attached.
|
||||||
const timeStamp = e.timeStamp || _getNow()
|
if (!e._vts) {
|
||||||
|
e._vts = Date.now()
|
||||||
if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
|
} else if (e._vts <= invoker.attached) {
|
||||||
callWithAsyncErrorHandling(
|
return
|
||||||
patchStopImmediatePropagation(e, invoker.value),
|
|
||||||
instance,
|
|
||||||
ErrorCodes.NATIVE_EVENT_HANDLER,
|
|
||||||
[e]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
callWithAsyncErrorHandling(
|
||||||
|
patchStopImmediatePropagation(e, invoker.value),
|
||||||
|
instance,
|
||||||
|
ErrorCodes.NATIVE_EVENT_HANDLER,
|
||||||
|
[e]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
invoker.value = initialValue
|
invoker.value = initialValue
|
||||||
invoker.attached = getNow()
|
invoker.attached = getNow()
|
||||||
|
|
Loading…
Reference in New Issue