componentEmits.ts 7.1 KB

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