componentEmits.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import {
  2. EMPTY_OBJ,
  3. type UnionToIntersection,
  4. camelize,
  5. extend,
  6. hasOwn,
  7. hyphenate,
  8. isArray,
  9. isFunction,
  10. isObject,
  11. isOn,
  12. isString,
  13. looseToNumber,
  14. toHandlerKey,
  15. } from '@vue/shared'
  16. import {
  17. type ComponentInternalInstance,
  18. type ComponentOptions,
  19. type ConcreteComponent,
  20. formatComponentName,
  21. } from './component'
  22. import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
  23. import { warn } from './warning'
  24. import { devtoolsComponentEmit } from './devtools'
  25. import type { AppContext } from './apiCreateApp'
  26. import { emit as compatInstanceEmit } from './compat/instanceEventEmitter'
  27. import {
  28. compatModelEmit,
  29. compatModelEventPrefix,
  30. } from './compat/componentVModel'
  31. export type ObjectEmitsOptions = Record<
  32. string,
  33. ((...args: any[]) => any) | null
  34. >
  35. export type EmitsOptions = ObjectEmitsOptions | string[]
  36. export type EmitsToProps<T extends EmitsOptions> = T extends string[]
  37. ? {
  38. [K in `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
  39. }
  40. : T extends ObjectEmitsOptions
  41. ? {
  42. [K in `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
  43. ? (
  44. ...args: T[Uncapitalize<C>] extends (...args: infer P) => any
  45. ? P
  46. : T[Uncapitalize<C>] extends null
  47. ? any[]
  48. : never
  49. ) => any
  50. : never
  51. }
  52. : {}
  53. export type ShortEmitsToObject<E> =
  54. E extends Record<string, any[]>
  55. ? {
  56. [K in keyof E]: (...args: E[K]) => any
  57. }
  58. : E
  59. export type EmitFn<
  60. Options = ObjectEmitsOptions,
  61. Event extends keyof Options = keyof Options,
  62. > =
  63. Options extends Array<infer V>
  64. ? (event: V, ...args: any[]) => void
  65. : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
  66. ? (event: string, ...args: any[]) => void
  67. : UnionToIntersection<
  68. {
  69. [key in Event]: Options[key] extends (...args: infer Args) => any
  70. ? (event: key, ...args: Args) => void
  71. : Options[key] extends any[]
  72. ? (event: key, ...args: Options[key]) => void
  73. : (event: key, ...args: any[]) => void
  74. }[Event]
  75. >
  76. export function emit(
  77. instance: ComponentInternalInstance,
  78. event: string,
  79. ...rawArgs: any[]
  80. ) {
  81. if (instance.isUnmounted) return
  82. const props = instance.vnode.props || EMPTY_OBJ
  83. if (__DEV__) {
  84. const {
  85. emitsOptions,
  86. propsOptions: [propsOptions],
  87. } = instance
  88. if (emitsOptions) {
  89. if (
  90. !(event in emitsOptions) &&
  91. !(
  92. __COMPAT__ &&
  93. (event.startsWith('hook:') ||
  94. event.startsWith(compatModelEventPrefix))
  95. )
  96. ) {
  97. if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
  98. warn(
  99. `Component emitted event "${event}" but it is neither declared in ` +
  100. `the emits option nor as an "${toHandlerKey(event)}" prop.`,
  101. )
  102. }
  103. } else {
  104. const validator = emitsOptions[event]
  105. if (isFunction(validator)) {
  106. const isValid = validator(...rawArgs)
  107. if (!isValid) {
  108. warn(
  109. `Invalid event arguments: event validation failed for event "${event}".`,
  110. )
  111. }
  112. }
  113. }
  114. }
  115. }
  116. let args = rawArgs
  117. const isModelListener = event.startsWith('update:')
  118. // for v-model update:xxx events, apply modifiers on args
  119. const modelArg = isModelListener && event.slice(7)
  120. if (modelArg && modelArg in props) {
  121. const modifiersKey = `${
  122. modelArg === 'modelValue' ? 'model' : modelArg
  123. }Modifiers`
  124. const { number, trim } = props[modifiersKey] || EMPTY_OBJ
  125. if (trim) {
  126. args = rawArgs.map(a => (isString(a) ? a.trim() : a))
  127. }
  128. if (number) {
  129. args = rawArgs.map(looseToNumber)
  130. }
  131. }
  132. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  133. devtoolsComponentEmit(instance, event, args)
  134. }
  135. if (__DEV__) {
  136. const lowerCaseEvent = event.toLowerCase()
  137. if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
  138. warn(
  139. `Event "${lowerCaseEvent}" is emitted in component ` +
  140. `${formatComponentName(
  141. instance,
  142. instance.type,
  143. )} but the handler is registered for "${event}". ` +
  144. `Note that HTML attributes are case-insensitive and you cannot use ` +
  145. `v-on to listen to camelCase events when using in-DOM templates. ` +
  146. `You should probably use "${hyphenate(
  147. event,
  148. )}" instead of "${event}".`,
  149. )
  150. }
  151. }
  152. let handlerName
  153. let handler =
  154. props[(handlerName = toHandlerKey(event))] ||
  155. // also try camelCase event handler (#2249)
  156. props[(handlerName = toHandlerKey(camelize(event)))]
  157. // for v-model update:xxx events, also trigger kebab-case equivalent
  158. // for props passed via kebab-case
  159. if (!handler && isModelListener) {
  160. handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
  161. }
  162. if (handler) {
  163. callWithAsyncErrorHandling(
  164. handler,
  165. instance,
  166. ErrorCodes.COMPONENT_EVENT_HANDLER,
  167. args,
  168. )
  169. }
  170. const onceHandler = props[handlerName + `Once`]
  171. if (onceHandler) {
  172. if (!instance.emitted) {
  173. instance.emitted = {}
  174. } else if (instance.emitted[handlerName]) {
  175. return
  176. }
  177. instance.emitted[handlerName] = true
  178. callWithAsyncErrorHandling(
  179. onceHandler,
  180. instance,
  181. ErrorCodes.COMPONENT_EVENT_HANDLER,
  182. args,
  183. )
  184. }
  185. if (__COMPAT__) {
  186. compatModelEmit(instance, event, args)
  187. return compatInstanceEmit(instance, event, args)
  188. }
  189. }
  190. export function normalizeEmitsOptions(
  191. comp: ConcreteComponent,
  192. appContext: AppContext,
  193. asMixin = false,
  194. ): ObjectEmitsOptions | null {
  195. const cache = appContext.emitsCache
  196. const cached = cache.get(comp)
  197. if (cached !== undefined) {
  198. return cached
  199. }
  200. const raw = comp.emits
  201. let normalized: ObjectEmitsOptions = {}
  202. // apply mixin/extends props
  203. let hasExtends = false
  204. if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
  205. const extendEmits = (raw: ComponentOptions) => {
  206. const normalizedFromExtend = normalizeEmitsOptions(raw, appContext, true)
  207. if (normalizedFromExtend) {
  208. hasExtends = true
  209. extend(normalized, normalizedFromExtend)
  210. }
  211. }
  212. if (!asMixin && appContext.mixins.length) {
  213. appContext.mixins.forEach(extendEmits)
  214. }
  215. if (comp.extends) {
  216. extendEmits(comp.extends)
  217. }
  218. if (comp.mixins) {
  219. comp.mixins.forEach(extendEmits)
  220. }
  221. }
  222. if (!raw && !hasExtends) {
  223. if (isObject(comp)) {
  224. cache.set(comp, null)
  225. }
  226. return null
  227. }
  228. if (isArray(raw)) {
  229. raw.forEach(key => (normalized[key] = null))
  230. } else {
  231. extend(normalized, raw)
  232. }
  233. if (isObject(comp)) {
  234. cache.set(comp, normalized)
  235. }
  236. return normalized
  237. }
  238. // Check if an incoming prop key is a declared emit event listener.
  239. // e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
  240. // both considered matched listeners.
  241. export function isEmitListener(
  242. options: ObjectEmitsOptions | null,
  243. key: string,
  244. ): boolean {
  245. if (!options || !isOn(key)) {
  246. return false
  247. }
  248. if (__COMPAT__ && key.startsWith(compatModelEventPrefix)) {
  249. return true
  250. }
  251. key = key.slice(2).replace(/Once$/, '')
  252. return (
  253. hasOwn(options, key[0].toLowerCase() + key.slice(1)) ||
  254. hasOwn(options, hyphenate(key)) ||
  255. hasOwn(options, key)
  256. )
  257. }