1
- const delegateRE = / ^ (?: c l i c k | d b l c l i c k | s u b m i t | (?: k e y | m o u s e | t o u c h | p o i n t e r ) . * ) $ /
1
+ import { isChrome } from '../ua'
2
2
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
+ }
5
11
6
12
export function patchEvent (
7
13
el : Element ,
8
14
name : string ,
9
15
prevValue : EventValue | null ,
10
16
nextValue : EventValue | null
11
17
) {
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 ) )
43
29
}
44
- store [ name ] = null
30
+ } else if ( invoker ) {
31
+ el . removeEventListener ( name , invoker as any )
45
32
}
46
33
}
47
34
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 ( )
73
43
}
44
+ return invoker
74
45
}
75
46
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
100
56
}
101
- }
102
57
103
- function invokeEvents ( e : Event , value : EventValue ) {
104
58
if ( Array . isArray ( value ) ) {
105
59
for ( let i = 0 ; i < value . length ; i ++ ) {
106
60
value [ i ] ( e )
@@ -109,32 +63,3 @@ function invokeEvents(e: Event, value: EventValue) {
109
63
value ( e )
110
64
}
111
65
}
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
- }
0 commit comments