2
0

vOn.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { DirectiveTransform, DirectiveTransformResult } from '../transform'
  2. import {
  3. createCompoundExpression,
  4. createObjectProperty,
  5. createSimpleExpression,
  6. DirectiveNode,
  7. ElementTypes,
  8. ExpressionNode,
  9. NodeTypes,
  10. SimpleExpressionNode
  11. } from '../ast'
  12. import { camelize, toHandlerKey } from '@vue/shared'
  13. import { createCompilerError, ErrorCodes } from '../errors'
  14. import { processExpression } from './transformExpression'
  15. import { validateBrowserExpression } from '../validateExpression'
  16. import { hasScopeRef, isMemberExpression } from '../utils'
  17. import { TO_HANDLER_KEY } from '../runtimeHelpers'
  18. const fnExpRE =
  19. /^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
  20. export interface VOnDirectiveNode extends DirectiveNode {
  21. // v-on without arg is handled directly in ./transformElements.ts due to it affecting
  22. // codegen for the entire props object. This transform here is only for v-on
  23. // *with* args.
  24. arg: ExpressionNode
  25. // exp is guaranteed to be a simple expression here because v-on w/ arg is
  26. // skipped by transformExpression as a special case.
  27. exp: SimpleExpressionNode | undefined
  28. }
  29. export const transformOn: DirectiveTransform = (
  30. dir,
  31. node,
  32. context,
  33. augmentor
  34. ) => {
  35. const { loc, modifiers, arg } = dir as VOnDirectiveNode
  36. if (!dir.exp && !modifiers.length) {
  37. context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
  38. }
  39. let eventName: ExpressionNode
  40. if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
  41. if (arg.isStatic) {
  42. let rawName = arg.content
  43. // TODO deprecate @vnodeXXX usage
  44. if (rawName.startsWith('vue:')) {
  45. rawName = `vnode-${rawName.slice(4)}`
  46. }
  47. const eventString =
  48. node.tagType === ElementTypes.COMPONENT ||
  49. rawName.startsWith('vnode') ||
  50. !/[A-Z]/.test(rawName)
  51. ? // for component and vnode lifecycle event listeners, auto convert
  52. // it to camelCase. See issue #2249
  53. toHandlerKey(camelize(rawName))
  54. : // preserve case for plain element listeners that have uppercase
  55. // letters, as these may be custom elements' custom events
  56. `on:${rawName}`
  57. eventName = createSimpleExpression(eventString, true, arg.loc)
  58. } else {
  59. // #2388
  60. eventName = createCompoundExpression([
  61. `${context.helperString(TO_HANDLER_KEY)}(`,
  62. arg,
  63. `)`
  64. ])
  65. }
  66. } else {
  67. // already a compound expression.
  68. eventName = arg
  69. eventName.children.unshift(`${context.helperString(TO_HANDLER_KEY)}(`)
  70. eventName.children.push(`)`)
  71. }
  72. // handler processing
  73. let exp: ExpressionNode | undefined = dir.exp as
  74. | SimpleExpressionNode
  75. | undefined
  76. if (exp && !exp.content.trim()) {
  77. exp = undefined
  78. }
  79. let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
  80. if (exp) {
  81. const isMemberExp = isMemberExpression(exp.content, context)
  82. const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
  83. const hasMultipleStatements = exp.content.includes(`;`)
  84. // process the expression since it's been skipped
  85. if (!__BROWSER__ && context.prefixIdentifiers) {
  86. isInlineStatement && context.addIdentifiers(`$event`)
  87. exp = dir.exp = processExpression(
  88. exp,
  89. context,
  90. false,
  91. hasMultipleStatements
  92. )
  93. isInlineStatement && context.removeIdentifiers(`$event`)
  94. // with scope analysis, the function is hoistable if it has no reference
  95. // to scope variables.
  96. shouldCache =
  97. context.cacheHandlers &&
  98. // unnecessary to cache inside v-once
  99. !context.inVOnce &&
  100. // runtime constants don't need to be cached
  101. // (this is analyzed by compileScript in SFC <script setup>)
  102. !(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.constType > 0) &&
  103. // #1541 bail if this is a member exp handler passed to a component -
  104. // we need to use the original function to preserve arity,
  105. // e.g. <transition> relies on checking cb.length to determine
  106. // transition end handling. Inline function is ok since its arity
  107. // is preserved even when cached.
  108. !(isMemberExp && node.tagType === ElementTypes.COMPONENT) &&
  109. // bail if the function references closure variables (v-for, v-slot)
  110. // it must be passed fresh to avoid stale values.
  111. !hasScopeRef(exp, context.identifiers)
  112. // If the expression is optimizable and is a member expression pointing
  113. // to a function, turn it into invocation (and wrap in an arrow function
  114. // below) so that it always accesses the latest value when called - thus
  115. // avoiding the need to be patched.
  116. if (shouldCache && isMemberExp) {
  117. if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  118. exp.content = `${exp.content} && ${exp.content}(...args)`
  119. } else {
  120. exp.children = [...exp.children, ` && `, ...exp.children, `(...args)`]
  121. }
  122. }
  123. }
  124. if (__DEV__ && __BROWSER__) {
  125. validateBrowserExpression(
  126. exp as SimpleExpressionNode,
  127. context,
  128. false,
  129. hasMultipleStatements
  130. )
  131. }
  132. if (isInlineStatement || (shouldCache && isMemberExp)) {
  133. // wrap inline statement in a function expression
  134. exp = createCompoundExpression([
  135. `${
  136. isInlineStatement
  137. ? !__BROWSER__ && context.isTS
  138. ? `($event: any)`
  139. : `$event`
  140. : `${
  141. !__BROWSER__ && context.isTS ? `\n//@ts-ignore\n` : ``
  142. }(...args)`
  143. } => ${hasMultipleStatements ? `{` : `(`}`,
  144. exp,
  145. hasMultipleStatements ? `}` : `)`
  146. ])
  147. }
  148. }
  149. let ret: DirectiveTransformResult = {
  150. props: [
  151. createObjectProperty(
  152. eventName,
  153. exp || createSimpleExpression(`() => {}`, false, loc)
  154. )
  155. ]
  156. }
  157. // apply extended compiler augmentor
  158. if (augmentor) {
  159. ret = augmentor(ret)
  160. }
  161. if (shouldCache) {
  162. // cache handlers so that it's always the same handler being passed down.
  163. // this avoids unnecessary re-renders when users use inline handlers on
  164. // components.
  165. ret.props[0].value = context.cache(ret.props[0].value)
  166. }
  167. // mark the key as handler for props normalization check
  168. ret.props.forEach(p => (p.key.isHandlerKey = true))
  169. return ret
  170. }