refactor: drop event delegation and use simple async edge case fix
This commit is contained in:
parent
9449dfb352
commit
360a10fff2
@ -72,7 +72,6 @@ export interface PatchDataFunction {
|
|||||||
export interface RendererOptions {
|
export interface RendererOptions {
|
||||||
nodeOps: NodeOps
|
nodeOps: NodeOps
|
||||||
patchData: PatchDataFunction
|
patchData: PatchDataFunction
|
||||||
teardownVNode?: (vnode: VNode) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionalHandle {
|
export interface FunctionalHandle {
|
||||||
@ -102,8 +101,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
nextSibling: platformNextSibling,
|
nextSibling: platformNextSibling,
|
||||||
querySelector: platformQuerySelector
|
querySelector: platformQuerySelector
|
||||||
},
|
},
|
||||||
patchData: platformPatchData,
|
patchData: platformPatchData
|
||||||
teardownVNode
|
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
function queueInsertOrAppend(
|
function queueInsertOrAppend(
|
||||||
@ -1138,9 +1136,6 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
data.vnodeBeforeUnmount(vnode)
|
data.vnodeBeforeUnmount(vnode)
|
||||||
}
|
}
|
||||||
unmountChildren(children as VNodeChildren, childFlags)
|
unmountChildren(children as VNodeChildren, childFlags)
|
||||||
if (teardownVNode !== void 0) {
|
|
||||||
teardownVNode(vnode)
|
|
||||||
}
|
|
||||||
if (isElement && data != null && data.vnodeUnmounted) {
|
if (isElement && data != null && data.vnodeUnmounted) {
|
||||||
data.vnodeUnmounted(vnode)
|
data.vnodeUnmounted(vnode)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { createRenderer, Component } from '@vue/runtime-core'
|
import { createRenderer, Component } from '@vue/runtime-core'
|
||||||
import { nodeOps } from './nodeOps'
|
import { nodeOps } from './nodeOps'
|
||||||
import { patchData } from './patchData'
|
import { patchData } from './patchData'
|
||||||
import { teardownVNode } from './teardownVNode'
|
|
||||||
|
|
||||||
const { render: _render } = createRenderer({
|
const { render: _render } = createRenderer({
|
||||||
nodeOps,
|
nodeOps,
|
||||||
patchData,
|
patchData
|
||||||
teardownVNode
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type publicRender = (
|
type publicRender = (
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
|
import { isChrome } from '../ua'
|
||||||
|
|
||||||
type EventValue = Function | Function[]
|
interface Invoker extends Function {
|
||||||
type TargetRef = { el: Element | Document }
|
value: EventValue
|
||||||
|
lastUpdated?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventValue = (Function | Function[]) & {
|
||||||
|
invoker?: Invoker | null
|
||||||
|
}
|
||||||
|
|
||||||
export function patchEvent(
|
export function patchEvent(
|
||||||
el: Element,
|
el: Element,
|
||||||
@ -9,98 +15,46 @@ export function patchEvent(
|
|||||||
prevValue: EventValue | null,
|
prevValue: EventValue | null,
|
||||||
nextValue: EventValue | null
|
nextValue: EventValue | null
|
||||||
) {
|
) {
|
||||||
if (delegateRE.test(name) && !__JSDOM__) {
|
const invoker = prevValue && prevValue.invoker
|
||||||
handleDelegatedEvent(el, name, nextValue)
|
if (nextValue) {
|
||||||
} else {
|
if (invoker) {
|
||||||
handleNormalEvent(el, name, prevValue, nextValue)
|
;(prevValue as EventValue).invoker = null
|
||||||
}
|
invoker.value = nextValue
|
||||||
}
|
nextValue.invoker = invoker
|
||||||
|
if (isChrome) {
|
||||||
const eventCounts: Record<string, number> = {}
|
invoker.lastUpdated = performance.now()
|
||||||
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)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
dispatchEvent(e, name, isClick, targetRef)
|
el.addEventListener(name, createInvoker(nextValue))
|
||||||
})
|
}
|
||||||
document.addEventListener(name, handler)
|
} else if (invoker) {
|
||||||
eventCounts[name] = 0
|
el.removeEventListener(name, invoker as any)
|
||||||
}
|
|
||||||
|
|
||||||
function stopPropagation() {
|
|
||||||
this.cancelBubble = true
|
|
||||||
if (!this.immediatePropagationStopped) {
|
|
||||||
this.stopImmediatePropagation()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchEvent(
|
function createInvoker(value: any) {
|
||||||
e: Event,
|
const invoker = ((e: Event) => {
|
||||||
name: string,
|
invokeEvents(e, invoker.value, invoker.lastUpdated)
|
||||||
isClick: boolean,
|
}) as any
|
||||||
targetRef: TargetRef
|
invoker.value = value
|
||||||
) {
|
value.invoker = invoker
|
||||||
let el = e.target as any
|
if (isChrome) {
|
||||||
while (el != null) {
|
invoker.lastUpdated = performance.now()
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
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)) {
|
if (Array.isArray(value)) {
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
value[i](e)
|
value[i](e)
|
||||||
@ -109,32 +63,3 @@ function invokeEvents(e: Event, value: EventValue) {
|
|||||||
value(e)
|
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 +1,5 @@
|
|||||||
import { isString } from '@vue/shared'
|
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) {
|
export function patchStyle(el: any, prev: any, next: any, data: any) {
|
||||||
const { style } = el
|
const { style } = el
|
||||||
if (!next) {
|
if (!next) {
|
||||||
@ -11,11 +8,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
|
|||||||
style.cssText = next
|
style.cssText = next
|
||||||
} else {
|
} else {
|
||||||
for (const key in next) {
|
for (const key in next) {
|
||||||
let value = next[key]
|
style[key] = next[key]
|
||||||
if (typeof value === 'number' && !nonNumericRE.test(key)) {
|
|
||||||
value = value + 'px'
|
|
||||||
}
|
|
||||||
style[key] = value
|
|
||||||
}
|
}
|
||||||
if (prev && !isString(prev)) {
|
if (prev && !isString(prev)) {
|
||||||
for (const key in prev) {
|
for (const key in prev) {
|
||||||
|
@ -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
packages/runtime-dom/src/ua.ts
Normal file
3
packages/runtime-dom/src/ua.ts
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user