-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
/
events.ts
148 lines (134 loc) · 4.61 KB
/
events.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { hyphenate, isArray } from '@vue/shared'
import {
ComponentInternalInstance,
callWithAsyncErrorHandling
} from '@vue/runtime-core'
import { ErrorCodes } from 'packages/runtime-core/src/errorHandling'
interface Invoker extends EventListener {
value: EventValue
attached: number
}
type EventValue = Function | Function[]
// Async edge case fix requires storing an event listener's attach timestamp.
let _getNow: () => number = Date.now
let skipTimestampCheck = false
if (typeof window !== 'undefined') {
// Determine what event timestamp the browser is using. Annoyingly, the
// timestamp can either be hi-res (relative to page load) or low-res
// (relative to UNIX epoch), so in order to compare time we have to use the
// same timestamp type when saving the flush timestamp.
if (_getNow() > document.createEvent('Event').timeStamp) {
// if the low-res timestamp which is bigger than the event timestamp
// (which is evaluated AFTER) it means the event is using a hi-res timestamp,
// and we need to use the hi-res version for event listeners as well.
_getNow = () => performance.now()
}
// #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
// and does not fire microtasks in between event propagation, so safe to exclude.
const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i)
skipTimestampCheck = !!(ffMatch && Number(ffMatch[1]) <= 53)
}
// To avoid the overhead of repeatedly calling performance.now(), we cache
// and use the same timestamp for all event listeners attached in the same tick.
let cachedNow: number = 0
const p = Promise.resolve()
const reset = () => {
cachedNow = 0
}
const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
export function addEventListener(
el: Element,
event: string,
handler: EventListener,
options?: EventListenerOptions
) {
el.addEventListener(event, handler, options)
}
export function removeEventListener(
el: Element,
event: string,
handler: EventListener,
options?: EventListenerOptions
) {
el.removeEventListener(event, handler, options)
}
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
const optionsModifierRE = /(?:Once|Passive|Capture)$/
function parseName(name: string): [string, EventListenerOptions | undefined] {
let options: EventListenerOptions | undefined
if (optionsModifierRE.test(name)) {
options = {}
let m
while ((m = name.match(optionsModifierRE))) {
name = name.slice(0, name.length - m[0].length)
;(options as any)[m[0].toLowerCase()] = true
options
}
}
return [hyphenate(name.slice(2)), options]
}
function createInvoker(
initialValue: EventValue,
instance: ComponentInternalInstance | null
) {
const invoker: Invoker = (e: Event) => {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire 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.
const timeStamp = e.timeStamp || _getNow()
if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
callWithAsyncErrorHandling(
patchStopImmediatePropagation(e, invoker.value),
instance,
ErrorCodes.NATIVE_EVENT_HANDLER,
[e]
)
}
}
invoker.value = initialValue
invoker.attached = getNow()
return invoker
}
function patchStopImmediatePropagation(
e: Event,
value: EventValue
): EventValue {
if (isArray(value)) {
const originalStop = e.stopImmediatePropagation
e.stopImmediatePropagation = () => {
originalStop.call(e)
;(e as any)._stopped = true
}
return value.map(fn => (e: Event) => !(e as any)._stopped && fn(e))
} else {
return value
}
}