close https://github.com/vuejs/docs/issues/1708 close https://github.com/vuejs/docs/pull/1890
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
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.
|
|
const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
|
|
let _getNow = 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 (Date.now() > 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.bind(performance)
|
|
}
|
|
// #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)
|
|
}
|
|
return [_getNow, skipTimestampCheck]
|
|
})()
|
|
|
|
// 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 = /*#__PURE__*/ 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
|
|
}
|
|
}
|
|
const event = name[2] === ':' ? name.slice(3) : hyphenate(name.slice(2))
|
|
return [event, 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 && fn(e))
|
|
} else {
|
|
return value
|
|
}
|
|
}
|