]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-dom): fix event timestamp check in iframes
authorEvan You <yyx990803@gmail.com>
Fri, 14 Oct 2022 08:00:03 +0000 (16:00 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 14 Oct 2022 08:00:03 +0000 (16:00 +0800)
fix #2513
fix #3933
close #5474

packages/runtime-dom/__tests__/patchEvents.spec.ts
packages/runtime-dom/src/modules/events.ts

index 9c30616a24aa0a38c4b9d30b213f742d1effb9bb..32466f29a7bc739430aafc6e5226f6076ef26ec5 100644 (file)
@@ -5,30 +5,28 @@ const timeout = () => new Promise(r => setTimeout(r))
 describe(`runtime-dom: events patching`, () => {
   it('should assign event handler', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn = jest.fn()
     patchProp(el, 'onClick', null, fn)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn).toHaveBeenCalledTimes(3)
   })
 
   it('should update event handler', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const prevFn = jest.fn()
     const nextFn = jest.fn()
     patchProp(el, 'onClick', null, prevFn)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     patchProp(el, 'onClick', prevFn, nextFn)
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(prevFn).toHaveBeenCalledTimes(1)
     expect(nextFn).toHaveBeenCalledTimes(2)
@@ -36,11 +34,10 @@ describe(`runtime-dom: events patching`, () => {
 
   it('should support multiple event handlers', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn1 = jest.fn()
     const fn2 = jest.fn()
     patchProp(el, 'onClick', null, [fn1, fn2])
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn1).toHaveBeenCalledTimes(1)
     expect(fn2).toHaveBeenCalledTimes(1)
@@ -48,58 +45,55 @@ describe(`runtime-dom: events patching`, () => {
 
   it('should unassign event handler', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn = jest.fn()
     patchProp(el, 'onClick', null, fn)
     patchProp(el, 'onClick', fn, null)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn).not.toHaveBeenCalled()
   })
 
   it('should support event option modifiers', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn = jest.fn()
     patchProp(el, 'onClickOnceCapture', null, fn)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn).toHaveBeenCalledTimes(1)
   })
 
   it('should unassign event handler with options', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn = jest.fn()
     patchProp(el, 'onClickCapture', null, fn)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn).toHaveBeenCalledTimes(1)
 
     patchProp(el, 'onClickCapture', fn, null)
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn).toHaveBeenCalledTimes(1)
   })
 
   it('should support native onclick', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
 
     // string should be set as attribute
     const fn = ((window as any).__globalSpy = jest.fn())
     patchProp(el, 'onclick', null, '__globalSpy(1)')
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     delete (window as any).__globalSpy
     expect(fn).toHaveBeenCalledWith(1)
 
     const fn2 = jest.fn()
     patchProp(el, 'onclick', '__globalSpy(1)', fn2)
+    const event = new Event('click')
     el.dispatchEvent(event)
     await timeout()
     expect(fn).toHaveBeenCalledTimes(1)
@@ -108,13 +102,12 @@ describe(`runtime-dom: events patching`, () => {
 
   it('should support stopImmediatePropagation on multiple listeners', async () => {
     const el = document.createElement('div')
-    const event = new Event('click')
     const fn1 = jest.fn((e: Event) => {
       e.stopImmediatePropagation()
     })
     const fn2 = jest.fn()
     patchProp(el, 'onClick', null, [fn1, fn2])
-    el.dispatchEvent(event)
+    el.dispatchEvent(new Event('click'))
     await timeout()
     expect(fn1).toHaveBeenCalledTimes(1)
     expect(fn2).toHaveBeenCalledTimes(0)
@@ -125,15 +118,15 @@ describe(`runtime-dom: events patching`, () => {
     const el1 = document.createElement('div')
     const el2 = document.createElement('div')
 
-    const event = new Event('click')
+    // const event = new Event('click')
     const prevFn = jest.fn()
     const nextFn = jest.fn()
 
     patchProp(el1, 'onClick', null, prevFn)
     patchProp(el2, 'onClick', null, prevFn)
 
-    el1.dispatchEvent(event)
-    el2.dispatchEvent(event)
+    el1.dispatchEvent(new Event('click'))
+    el2.dispatchEvent(new Event('click'))
     await timeout()
     expect(prevFn).toHaveBeenCalledTimes(2)
     expect(nextFn).toHaveBeenCalledTimes(0)
@@ -141,19 +134,39 @@ describe(`runtime-dom: events patching`, () => {
     patchProp(el1, 'onClick', prevFn, nextFn)
     patchProp(el2, 'onClick', prevFn, nextFn)
 
-    el1.dispatchEvent(event)
-    el2.dispatchEvent(event)
+    el1.dispatchEvent(new Event('click'))
+    el2.dispatchEvent(new Event('click'))
     await timeout()
     expect(prevFn).toHaveBeenCalledTimes(2)
     expect(nextFn).toHaveBeenCalledTimes(2)
 
-    el1.dispatchEvent(event)
-    el2.dispatchEvent(event)
+    el1.dispatchEvent(new Event('click'))
+    el2.dispatchEvent(new Event('click'))
     await timeout()
     expect(prevFn).toHaveBeenCalledTimes(2)
     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
   test('should patch event correctly in web-components', async () => {
     class TestElement extends HTMLElement {
index d0f8d364a2983eee2079516344da6c6cb7a53fa8..8dbccadef1ac3e8a9c7dec8e4da8aa8e326c78be 100644 (file)
@@ -12,38 +12,6 @@ interface Invoker extends EventListener {
 
 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(
   el: Element,
   event: string,
@@ -105,27 +73,41 @@ function parseName(name: string): [string, EventListenerOptions | undefined] {
   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(
   initialValue: EventValue,
   instance: ComponentInternalInstance | null
 ) {
-  const invoker: Invoker = (e: Event) => {
-    // async edge case #6566: inner click event triggers patch, event handler
+  const invoker: Invoker = (e: Event & { _vts?: number }) => {
+    // async edge case vuejs/vue#6566
+    // inner click event triggers patch, event handler
     // attached to outer element during patch, and triggered again. This
     // happens because browsers fire 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
+    // this no longer happens for templates in Vue 3, but could still be
+    // 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.
-    const timeStamp = e.timeStamp || _getNow()
-
-    if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
-      callWithAsyncErrorHandling(
-        patchStopImmediatePropagation(e, invoker.value),
-        instance,
-        ErrorCodes.NATIVE_EVENT_HANDLER,
-        [e]
-      )
+    if (!e._vts) {
+      e._vts = Date.now()
+    } else if (e._vts <= invoker.attached) {
+      return
     }
+    callWithAsyncErrorHandling(
+      patchStopImmediatePropagation(e, invoker.value),
+      instance,
+      ErrorCodes.NATIVE_EVENT_HANDLER,
+      [e]
+    )
   }
   invoker.value = initialValue
   invoker.attached = getNow()