events.ts 4.6 KB

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