diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 8abdf01f..b9331017 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/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) } diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 38e35326..8b8f591d 100644 --- a/packages/runtime-dom/src/index.ts +++ b/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 = ( diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index a23e1166..77fbce8c 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/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 = {} -const attachedGlobalHandlers: Record = {} - -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) - } - store[name] = null - } -} - -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 + 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() } - }) - dispatchEvent(e, name, isClick, targetRef) - }) - document.addEventListener(name, handler) - eventCounts[name] = 0 -} - -function stopPropagation() { - this.cancelBubble = true - if (!this.immediatePropagationStopped) { - this.stopImmediatePropagation() + } else { + el.addEventListener(name, createInvoker(nextValue)) + } + } else if (invoker) { + el.removeEventListener(name, invoker as any) } } -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 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 invokeEvents(e: Event, value: EventValue) { +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 + } + 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 -} diff --git a/packages/runtime-dom/src/modules/style.ts b/packages/runtime-dom/src/modules/style.ts index e5c3e0ee..c55afd3d 100644 --- a/packages/runtime-dom/src/modules/style.ts +++ b/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) { diff --git a/packages/runtime-dom/src/teardownVNode.ts b/packages/runtime-dom/src/teardownVNode.ts deleted file mode 100644 index d53a0ef7..00000000 --- a/packages/runtime-dom/src/teardownVNode.ts +++ /dev/null @@ -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) - } - } - } -} diff --git a/packages/runtime-dom/src/ua.ts b/packages/runtime-dom/src/ua.ts new file mode 100644 index 00000000..62bfee86 --- /dev/null +++ b/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