componentEmits.ts 7.3 KB

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