componentEmits.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import {
  2. isArray,
  3. isOn,
  4. hasOwn,
  5. EMPTY_OBJ,
  6. capitalize,
  7. hyphenate,
  8. isFunction,
  9. extend
  10. } from '@vue/shared'
  11. import {
  12. ComponentInternalInstance,
  13. ComponentOptions,
  14. ConcreteComponent
  15. } from './component'
  16. import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
  17. import { warn } from './warning'
  18. import { UnionToIntersection } from './helpers/typeUtils'
  19. import { devtoolsComponentEmit } from './devtools'
  20. import { AppContext } from './apiCreateApp'
  21. export type ObjectEmitsOptions = Record<
  22. string,
  23. ((...args: any[]) => any) | null
  24. >
  25. export type EmitsOptions = ObjectEmitsOptions | string[]
  26. export type EmitFn<
  27. Options = ObjectEmitsOptions,
  28. Event extends keyof Options = keyof Options
  29. > = Options extends any[]
  30. ? (event: Options[0], ...args: any[]) => void
  31. : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
  32. ? (event: string, ...args: any[]) => void
  33. : UnionToIntersection<
  34. {
  35. [key in Event]: Options[key] extends ((...args: infer Args) => any)
  36. ? (event: key, ...args: Args) => void
  37. : (event: key, ...args: any[]) => void
  38. }[Event]
  39. >
  40. export function emit(
  41. instance: ComponentInternalInstance,
  42. event: string,
  43. ...args: any[]
  44. ) {
  45. const props = instance.vnode.props || EMPTY_OBJ
  46. if (__DEV__) {
  47. const {
  48. emitsOptions,
  49. propsOptions: [propsOptions]
  50. } = instance
  51. if (emitsOptions) {
  52. if (!(event in emitsOptions)) {
  53. if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) {
  54. warn(
  55. `Component emitted event "${event}" but it is neither declared in ` +
  56. `the emits option nor as an "on${capitalize(event)}" prop.`
  57. )
  58. }
  59. } else {
  60. const validator = emitsOptions[event]
  61. if (isFunction(validator)) {
  62. const isValid = validator(...args)
  63. if (!isValid) {
  64. warn(
  65. `Invalid event arguments: event validation failed for event "${event}".`
  66. )
  67. }
  68. }
  69. }
  70. }
  71. }
  72. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  73. devtoolsComponentEmit(instance, event, args)
  74. }
  75. let handlerName = `on${capitalize(event)}`
  76. let handler = props[handlerName]
  77. // for v-model update:xxx events, also trigger kebab-case equivalent
  78. // for props passed via kebab-case
  79. if (!handler && event.startsWith('update:')) {
  80. handlerName = `on${capitalize(hyphenate(event))}`
  81. handler = props[handlerName]
  82. }
  83. if (!handler) {
  84. handler = props[handlerName + `Once`]
  85. if (!instance.emitted) {
  86. ;(instance.emitted = {} as Record<string, boolean>)[handlerName] = true
  87. } else if (instance.emitted[handlerName]) {
  88. return
  89. }
  90. }
  91. if (handler) {
  92. callWithAsyncErrorHandling(
  93. handler,
  94. instance,
  95. ErrorCodes.COMPONENT_EVENT_HANDLER,
  96. args
  97. )
  98. }
  99. }
  100. export function normalizeEmitsOptions(
  101. comp: ConcreteComponent,
  102. appContext: AppContext,
  103. asMixin = false
  104. ): ObjectEmitsOptions | null {
  105. const appId = appContext.app ? appContext.app._uid : -1
  106. const cache = comp.__emits || (comp.__emits = {})
  107. const cached = cache[appId]
  108. if (cached !== undefined) {
  109. return cached
  110. }
  111. const raw = comp.emits
  112. let normalized: ObjectEmitsOptions = {}
  113. // apply mixin/extends props
  114. let hasExtends = false
  115. if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
  116. const extendEmits = (raw: ComponentOptions) => {
  117. hasExtends = true
  118. extend(normalized, normalizeEmitsOptions(raw, appContext, true))
  119. }
  120. if (!asMixin && appContext.mixins.length) {
  121. appContext.mixins.forEach(extendEmits)
  122. }
  123. if (comp.extends) {
  124. extendEmits(comp.extends)
  125. }
  126. if (comp.mixins) {
  127. comp.mixins.forEach(extendEmits)
  128. }
  129. }
  130. if (!raw && !hasExtends) {
  131. return (cache[appId] = null)
  132. }
  133. if (isArray(raw)) {
  134. raw.forEach(key => (normalized[key] = null))
  135. } else {
  136. extend(normalized, raw)
  137. }
  138. return (cache[appId] = normalized)
  139. }
  140. // Check if an incoming prop key is a declared emit event listener.
  141. // e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
  142. // both considered matched listeners.
  143. export function isEmitListener(
  144. options: ObjectEmitsOptions | null,
  145. key: string
  146. ): boolean {
  147. if (!options || !isOn(key)) {
  148. return false
  149. }
  150. key = key.replace(/Once$/, '')
  151. return (
  152. hasOwn(options, key[2].toLowerCase() + key.slice(3)) ||
  153. hasOwn(options, key.slice(2))
  154. )
  155. }