componentEmits.ts 6.9 KB

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