Skip to content

Commit 360a10f

Browse files
committed
refactor: drop event delegation and use simple async edge case fix
1 parent 9449dfb commit 360a10f

File tree

6 files changed

+46
-146
lines changed

6 files changed

+46
-146
lines changed

packages/runtime-core/src/createRenderer.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ export interface PatchDataFunction {
7272
export interface RendererOptions {
7373
nodeOps: NodeOps
7474
patchData: PatchDataFunction
75-
teardownVNode?: (vnode: VNode) => void
7675
}
7776

7877
export interface FunctionalHandle {
@@ -102,8 +101,7 @@ export function createRenderer(options: RendererOptions) {
102101
nextSibling: platformNextSibling,
103102
querySelector: platformQuerySelector
104103
},
105-
patchData: platformPatchData,
106-
teardownVNode
104+
patchData: platformPatchData
107105
} = options
108106

109107
function queueInsertOrAppend(
@@ -1138,9 +1136,6 @@ export function createRenderer(options: RendererOptions) {
11381136
data.vnodeBeforeUnmount(vnode)
11391137
}
11401138
unmountChildren(children as VNodeChildren, childFlags)
1141-
if (teardownVNode !== void 0) {
1142-
teardownVNode(vnode)
1143-
}
11441139
if (isElement && data != null && data.vnodeUnmounted) {
11451140
data.vnodeUnmounted(vnode)
11461141
}

packages/runtime-dom/src/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { createRenderer, Component } from '@vue/runtime-core'
22
import { nodeOps } from './nodeOps'
33
import { patchData } from './patchData'
4-
import { teardownVNode } from './teardownVNode'
54

65
const { render: _render } = createRenderer({
76
nodeOps,
8-
patchData,
9-
teardownVNode
7+
patchData
108
})
119

1210
type publicRender = (
+40-115
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,60 @@
1-
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
1+
import { isChrome } from '../ua'
22

3-
type EventValue = Function | Function[]
4-
type TargetRef = { el: Element | Document }
3+
interface Invoker extends Function {
4+
value: EventValue
5+
lastUpdated?: number
6+
}
7+
8+
type EventValue = (Function | Function[]) & {
9+
invoker?: Invoker | null
10+
}
511

612
export function patchEvent(
713
el: Element,
814
name: string,
915
prevValue: EventValue | null,
1016
nextValue: EventValue | null
1117
) {
12-
if (delegateRE.test(name) && !__JSDOM__) {
13-
handleDelegatedEvent(el, name, nextValue)
14-
} else {
15-
handleNormalEvent(el, name, prevValue, nextValue)
16-
}
17-
}
18-
19-
const eventCounts: Record<string, number> = {}
20-
const attachedGlobalHandlers: Record<string, Function | null> = {}
21-
22-
export function handleDelegatedEvent(
23-
el: any,
24-
name: string,
25-
value: EventValue | null
26-
) {
27-
const count = eventCounts[name]
28-
let store = el.__events
29-
if (value) {
30-
if (!count) {
31-
attachGlobalHandler(name)
32-
}
33-
if (!store) {
34-
store = el.__events = {}
35-
}
36-
if (!store[name]) {
37-
eventCounts[name]++
38-
}
39-
store[name] = value
40-
} else if (store && store[name]) {
41-
if (--eventCounts[name] === 0) {
42-
removeGlobalHandler(name)
18+
const invoker = prevValue && prevValue.invoker
19+
if (nextValue) {
20+
if (invoker) {
21+
;(prevValue as EventValue).invoker = null
22+
invoker.value = nextValue
23+
nextValue.invoker = invoker
24+
if (isChrome) {
25+
invoker.lastUpdated = performance.now()
26+
}
27+
} else {
28+
el.addEventListener(name, createInvoker(nextValue))
4329
}
44-
store[name] = null
30+
} else if (invoker) {
31+
el.removeEventListener(name, invoker as any)
4532
}
4633
}
4734

48-
function attachGlobalHandler(name: string) {
49-
const handler = (attachedGlobalHandlers[name] = (e: Event) => {
50-
const isClick = e.type === 'click' || e.type === 'dblclick'
51-
if (isClick && (e as MouseEvent).button !== 0) {
52-
e.stopPropagation()
53-
return false
54-
}
55-
e.stopPropagation = stopPropagation
56-
const targetRef: TargetRef = { el: document }
57-
Object.defineProperty(e, 'currentTarget', {
58-
configurable: true,
59-
get() {
60-
return targetRef.el
61-
}
62-
})
63-
dispatchEvent(e, name, isClick, targetRef)
64-
})
65-
document.addEventListener(name, handler)
66-
eventCounts[name] = 0
67-
}
68-
69-
function stopPropagation() {
70-
this.cancelBubble = true
71-
if (!this.immediatePropagationStopped) {
72-
this.stopImmediatePropagation()
35+
function createInvoker(value: any) {
36+
const invoker = ((e: Event) => {
37+
invokeEvents(e, invoker.value, invoker.lastUpdated)
38+
}) as any
39+
invoker.value = value
40+
value.invoker = invoker
41+
if (isChrome) {
42+
invoker.lastUpdated = performance.now()
7343
}
44+
return invoker
7445
}
7546

76-
function dispatchEvent(
77-
e: Event,
78-
name: string,
79-
isClick: boolean,
80-
targetRef: TargetRef
81-
) {
82-
let el = e.target as any
83-
while (el != null) {
84-
// Don't process clicks on disabled elements
85-
if (isClick && el.disabled) {
86-
break
87-
}
88-
const store = el.__events
89-
if (store) {
90-
const value = store[name]
91-
if (value) {
92-
targetRef.el = el
93-
invokeEvents(e, value)
94-
if (e.cancelBubble) {
95-
break
96-
}
97-
}
98-
}
99-
el = el.parentNode
47+
function invokeEvents(e: Event, value: EventValue, lastUpdated: number) {
48+
// async edge case #6566: inner click event triggers patch, event handler
49+
// attached to outer element during patch, and triggered again. This only
50+
// happens in Chrome as it fires microtask ticks between event propagation.
51+
// the solution is simple: we save the timestamp when a handler is attached,
52+
// and the handler would only fire if the event passed to it was fired
53+
// AFTER it was attached.
54+
if (isChrome && e.timeStamp < lastUpdated) {
55+
return
10056
}
101-
}
10257

103-
function invokeEvents(e: Event, value: EventValue) {
10458
if (Array.isArray(value)) {
10559
for (let i = 0; i < value.length; i++) {
10660
value[i](e)
@@ -109,32 +63,3 @@ function invokeEvents(e: Event, value: EventValue) {
10963
value(e)
11064
}
11165
}
112-
113-
function removeGlobalHandler(name: string) {
114-
document.removeEventListener(name, attachedGlobalHandlers[name] as any)
115-
attachedGlobalHandlers[name] = null
116-
}
117-
118-
function handleNormalEvent(el: Element, name: string, prev: any, next: any) {
119-
const invoker = prev && prev.invoker
120-
if (next) {
121-
if (invoker) {
122-
prev.invoker = null
123-
invoker.value = next
124-
next.invoker = invoker
125-
} else {
126-
el.addEventListener(name, createInvoker(next))
127-
}
128-
} else if (invoker) {
129-
el.removeEventListener(name, invoker)
130-
}
131-
}
132-
133-
function createInvoker(value: any) {
134-
const invoker = ((e: Event) => {
135-
invokeEvents(e, invoker.value)
136-
}) as any
137-
invoker.value = value
138-
value.invoker = invoker
139-
return invoker
140-
}

packages/runtime-dom/src/modules/style.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { isString } from '@vue/shared'
22

3-
// style properties that should NOT have "px" added when numeric
4-
const nonNumericRE = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i
5-
63
export function patchStyle(el: any, prev: any, next: any, data: any) {
74
const { style } = el
85
if (!next) {
@@ -11,11 +8,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
118
style.cssText = next
129
} else {
1310
for (const key in next) {
14-
let value = next[key]
15-
if (typeof value === 'number' && !nonNumericRE.test(key)) {
16-
value = value + 'px'
17-
}
18-
style[key] = value
11+
style[key] = next[key]
1912
}
2013
if (prev && !isString(prev)) {
2114
for (const key in prev) {

packages/runtime-dom/src/teardownVNode.ts

-14
This file was deleted.

packages/runtime-dom/src/ua.ts

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

0 commit comments

Comments
 (0)