Quellcode durchsuchen

refactor: drop event delegation and use simple async edge case fix

Evan You vor 7 Jahren
Ursprung
Commit
360a10fff2

+ 1 - 6
packages/runtime-core/src/createRenderer.ts

@@ -72,7 +72,6 @@ export interface PatchDataFunction {
 export interface RendererOptions {
   nodeOps: NodeOps
   patchData: PatchDataFunction
-  teardownVNode?: (vnode: VNode) => void
 }
 
 export interface FunctionalHandle {
@@ -102,8 +101,7 @@ export function createRenderer(options: RendererOptions) {
       nextSibling: platformNextSibling,
       querySelector: platformQuerySelector
     },
-    patchData: platformPatchData,
-    teardownVNode
+    patchData: platformPatchData
   } = options
 
   function queueInsertOrAppend(
@@ -1138,9 +1136,6 @@ export function createRenderer(options: RendererOptions) {
         data.vnodeBeforeUnmount(vnode)
       }
       unmountChildren(children as VNodeChildren, childFlags)
-      if (teardownVNode !== void 0) {
-        teardownVNode(vnode)
-      }
       if (isElement && data != null && data.vnodeUnmounted) {
         data.vnodeUnmounted(vnode)
       }

+ 1 - 3
packages/runtime-dom/src/index.ts

@@ -1,12 +1,10 @@
 import { createRenderer, Component } from '@vue/runtime-core'
 import { nodeOps } from './nodeOps'
 import { patchData } from './patchData'
-import { teardownVNode } from './teardownVNode'
 
 const { render: _render } = createRenderer({
   nodeOps,
-  patchData,
-  teardownVNode
+  patchData
 })
 
 type publicRender = (

+ 40 - 115
packages/runtime-dom/src/modules/events.ts

@@ -1,7 +1,13 @@
-const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
+import { isChrome } from '../ua'
 
-type EventValue = Function | Function[]
-type TargetRef = { el: Element | Document }
+interface Invoker extends Function {
+  value: EventValue
+  lastUpdated?: number
+}
+
+type EventValue = (Function | Function[]) & {
+  invoker?: Invoker | null
+}
 
 export function patchEvent(
   el: Element,
@@ -9,98 +15,46 @@ export function patchEvent(
   prevValue: EventValue | null,
   nextValue: EventValue | null
 ) {
-  if (delegateRE.test(name) && !__JSDOM__) {
-    handleDelegatedEvent(el, name, nextValue)
-  } else {
-    handleNormalEvent(el, name, prevValue, nextValue)
-  }
-}
-
-const eventCounts: Record<string, number> = {}
-const attachedGlobalHandlers: Record<string, Function | null> = {}
-
-export function handleDelegatedEvent(
-  el: any,
-  name: string,
-  value: EventValue | null
-) {
-  const count = eventCounts[name]
-  let store = el.__events
-  if (value) {
-    if (!count) {
-      attachGlobalHandler(name)
-    }
-    if (!store) {
-      store = el.__events = {}
-    }
-    if (!store[name]) {
-      eventCounts[name]++
-    }
-    store[name] = value
-  } else if (store && store[name]) {
-    if (--eventCounts[name] === 0) {
-      removeGlobalHandler(name)
+  const invoker = prevValue && prevValue.invoker
+  if (nextValue) {
+    if (invoker) {
+      ;(prevValue as EventValue).invoker = null
+      invoker.value = nextValue
+      nextValue.invoker = invoker
+      if (isChrome) {
+        invoker.lastUpdated = performance.now()
+      }
+    } else {
+      el.addEventListener(name, createInvoker(nextValue))
     }
-    store[name] = null
+  } else if (invoker) {
+    el.removeEventListener(name, invoker as any)
   }
 }
 
-function attachGlobalHandler(name: string) {
-  const handler = (attachedGlobalHandlers[name] = (e: Event) => {
-    const isClick = e.type === 'click' || e.type === 'dblclick'
-    if (isClick && (e as MouseEvent).button !== 0) {
-      e.stopPropagation()
-      return false
-    }
-    e.stopPropagation = stopPropagation
-    const targetRef: TargetRef = { el: document }
-    Object.defineProperty(e, 'currentTarget', {
-      configurable: true,
-      get() {
-        return targetRef.el
-      }
-    })
-    dispatchEvent(e, name, isClick, targetRef)
-  })
-  document.addEventListener(name, handler)
-  eventCounts[name] = 0
-}
-
-function stopPropagation() {
-  this.cancelBubble = true
-  if (!this.immediatePropagationStopped) {
-    this.stopImmediatePropagation()
+function createInvoker(value: any) {
+  const invoker = ((e: Event) => {
+    invokeEvents(e, invoker.value, invoker.lastUpdated)
+  }) as any
+  invoker.value = value
+  value.invoker = invoker
+  if (isChrome) {
+    invoker.lastUpdated = performance.now()
   }
+  return invoker
 }
 
-function dispatchEvent(
-  e: Event,
-  name: string,
-  isClick: boolean,
-  targetRef: TargetRef
-) {
-  let el = e.target as any
-  while (el != null) {
-    // Don't process clicks on disabled elements
-    if (isClick && el.disabled) {
-      break
-    }
-    const store = el.__events
-    if (store) {
-      const value = store[name]
-      if (value) {
-        targetRef.el = el
-        invokeEvents(e, value)
-        if (e.cancelBubble) {
-          break
-        }
-      }
-    }
-    el = el.parentNode
+function invokeEvents(e: Event, value: EventValue, lastUpdated: number) {
+  // 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 (isChrome && e.timeStamp < lastUpdated) {
+    return
   }
-}
 
-function invokeEvents(e: Event, value: EventValue) {
   if (Array.isArray(value)) {
     for (let i = 0; i < value.length; i++) {
       value[i](e)
@@ -109,32 +63,3 @@ function invokeEvents(e: Event, value: EventValue) {
     value(e)
   }
 }
-
-function removeGlobalHandler(name: string) {
-  document.removeEventListener(name, attachedGlobalHandlers[name] as any)
-  attachedGlobalHandlers[name] = null
-}
-
-function handleNormalEvent(el: Element, name: string, prev: any, next: any) {
-  const invoker = prev && prev.invoker
-  if (next) {
-    if (invoker) {
-      prev.invoker = null
-      invoker.value = next
-      next.invoker = invoker
-    } else {
-      el.addEventListener(name, createInvoker(next))
-    }
-  } else if (invoker) {
-    el.removeEventListener(name, invoker)
-  }
-}
-
-function createInvoker(value: any) {
-  const invoker = ((e: Event) => {
-    invokeEvents(e, invoker.value)
-  }) as any
-  invoker.value = value
-  value.invoker = invoker
-  return invoker
-}

+ 1 - 8
packages/runtime-dom/src/modules/style.ts

@@ -1,8 +1,5 @@
 import { isString } from '@vue/shared'
 
-// style properties that should NOT have "px" added when numeric
-const nonNumericRE = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i
-
 export function patchStyle(el: any, prev: any, next: any, data: any) {
   const { style } = el
   if (!next) {
@@ -11,11 +8,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
     style.cssText = next
   } else {
     for (const key in next) {
-      let value = next[key]
-      if (typeof value === 'number' && !nonNumericRE.test(key)) {
-        value = value + 'px'
-      }
-      style[key] = value
+      style[key] = next[key]
     }
     if (prev && !isString(prev)) {
       for (const key in prev) {

+ 0 - 14
packages/runtime-dom/src/teardownVNode.ts

@@ -1,14 +0,0 @@
-import { VNode } from '@vue/runtime-core'
-import { handleDelegatedEvent } from './modules/events'
-import { isOn } from '@vue/shared'
-
-export function teardownVNode(vnode: VNode) {
-  const { el, data } = vnode
-  if (data != null) {
-    for (const key in data) {
-      if (isOn(key)) {
-        handleDelegatedEvent(el, key.slice(2).toLowerCase(), null)
-      }
-    }
-  }
-}

+ 3 - 0
packages/runtime-dom/src/ua.ts

@@ -0,0 +1,3 @@
+export const UA = window.navigator.userAgent.toLowerCase()
+export const isEdge = UA.indexOf('edge/') > 0
+export const isChrome = /chrome\/\d+/.test(UA) && !isEdge