2
0

componentEmits.ts 6.3 KB

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