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