/
hoistStatic.ts
362 lines (342 loc) 路 11.4 KB
/
hoistStatic.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
import {
ConstantTypes,
RootNode,
NodeTypes,
TemplateChildNode,
SimpleExpressionNode,
ElementTypes,
PlainElementNode,
ComponentNode,
TemplateNode,
VNodeCall,
ParentNode,
JSChildNode,
CallExpression,
createArrayExpression
} from '../ast'
import { TransformContext } from '../transform'
import { PatchFlags, isString, isSymbol, isArray } from '@vue/shared'
import { getVNodeBlockHelper, getVNodeHelper, isSlotOutlet } from '../utils'
import {
OPEN_BLOCK,
GUARD_REACTIVE_PROPS,
NORMALIZE_CLASS,
NORMALIZE_PROPS,
NORMALIZE_STYLE
} from '../runtimeHelpers'
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0])
)
}
export function isSingleElementRoot(
root: RootNode,
child: TemplateChildNode
): child is PlainElementNode | ComponentNode | TemplateNode {
const { children } = root
return (
children.length === 1 &&
child.type === NodeTypes.ELEMENT &&
!isSlotOutlet(child)
)
}
function walk(
node: ParentNode,
context: TransformContext,
doNotHoistNode: boolean = false
) {
const { children } = node
const originalCount = children.length
let hoistedCount = 0
for (let i = 0; i < children.length; i++) {
const child = children[i]
// only plain elements & text calls are eligible for hoisting.
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
continue
}
} else {
// node may contain dynamic children, but its props may be eligible for
// hoisting.
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
}
// walk further
if (child.type === NodeTypes.ELEMENT) {
const isComponent = child.tagType === ElementTypes.COMPONENT
if (isComponent) {
context.scopes.vSlot++
}
walk(child, context)
if (isComponent) {
context.scopes.vSlot--
}
} else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
// Do not hoist v-if single child because it has to be a block
walk(
child.branches[i],
context,
child.branches[i].children.length === 1
)
}
}
}
if (hoistedCount && context.transformHoist) {
context.transformHoist(children, context, node)
}
// all children were hoisted - the entire children array is hoistable.
if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
node.codegenNode.children = context.hoist(
createArrayExpression(node.codegenNode.children)
)
}
}
export function getConstantType(
node: TemplateChildNode | SimpleExpressionNode,
context: TransformContext
): ConstantTypes {
const { constantCache } = context
switch (node.type) {
case NodeTypes.ELEMENT:
if (node.tagType !== ElementTypes.ELEMENT) {
return ConstantTypes.NOT_CONSTANT
}
const cached = constantCache.get(node)
if (cached !== undefined) {
return cached
}
const codegenNode = node.codegenNode!
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return ConstantTypes.NOT_CONSTANT
}
if (
codegenNode.isBlock &&
node.tag !== 'svg' &&
node.tag !== 'foreignObject'
) {
return ConstantTypes.NOT_CONSTANT
}
const flag = getPatchFlag(codegenNode)
if (!flag) {
let returnType = ConstantTypes.CAN_STRINGIFY
// Element itself has no patch flag. However we still need to check:
// 1. Even for a node with no patch flag, it is possible for it to contain
// non-hoistable expressions that refers to scope variables, e.g. compiler
// injected keys or cached event handlers. Therefore we need to always
// check the codegenNode's props to be sure.
const generatedPropsType = getGeneratedPropsConstantType(node, context)
if (generatedPropsType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (generatedPropsType < returnType) {
returnType = generatedPropsType
}
// 2. its children.
for (let i = 0; i < node.children.length; i++) {
const childType = getConstantType(node.children[i], context)
if (childType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (childType < returnType) {
returnType = childType
}
}
// 3. if the type is not already CAN_SKIP_PATCH which is the lowest non-0
// type, check if any of the props can cause the type to be lowered
// we can skip can_patch because it's guaranteed by the absence of a
// patchFlag.
if (returnType > ConstantTypes.CAN_SKIP_PATCH) {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.exp) {
const expType = getConstantType(p.exp, context)
if (expType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (expType < returnType) {
returnType = expType
}
}
}
}
// only svg/foreignObject could be block here, however if they are
// static then they don't need to be blocks since there will be no
// nested updates.
if (codegenNode.isBlock) {
// except set custom directives.
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.DIRECTIVE) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
}
context.removeHelper(OPEN_BLOCK)
context.removeHelper(
getVNodeBlockHelper(context.inSSR, codegenNode.isComponent)
)
codegenNode.isBlock = false
context.helper(getVNodeHelper(context.inSSR, codegenNode.isComponent))
}
constantCache.set(node, returnType)
return returnType
} else {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
case NodeTypes.TEXT:
case NodeTypes.COMMENT:
return ConstantTypes.CAN_STRINGIFY
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
return ConstantTypes.NOT_CONSTANT
case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
return getConstantType(node.content, context)
case NodeTypes.SIMPLE_EXPRESSION:
return node.constType
case NodeTypes.COMPOUND_EXPRESSION:
let returnType = ConstantTypes.CAN_STRINGIFY
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (isString(child) || isSymbol(child)) {
continue
}
const childType = getConstantType(child, context)
if (childType === ConstantTypes.NOT_CONSTANT) {
return ConstantTypes.NOT_CONSTANT
} else if (childType < returnType) {
returnType = childType
}
}
return returnType
default:
if (__DEV__) {
const exhaustiveCheck: never = node
exhaustiveCheck
}
return ConstantTypes.NOT_CONSTANT
}
}
const allowHoistedHelperSet = new Set([
NORMALIZE_CLASS,
NORMALIZE_STYLE,
NORMALIZE_PROPS,
GUARD_REACTIVE_PROPS
])
function getConstantTypeOfHelperCall(
value: CallExpression,
context: TransformContext
): ConstantTypes {
if (
value.type === NodeTypes.JS_CALL_EXPRESSION &&
!isString(value.callee) &&
allowHoistedHelperSet.has(value.callee)
) {
const arg = value.arguments[0] as JSChildNode
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
return getConstantType(arg, context)
} else if (arg.type === NodeTypes.JS_CALL_EXPRESSION) {
// in the case of nested helper call, e.g. `normalizeProps(guardReactiveProps(exp))`
return getConstantTypeOfHelperCall(arg, context)
}
}
return ConstantTypes.NOT_CONSTANT
}
function getGeneratedPropsConstantType(
node: PlainElementNode,
context: TransformContext
): ConstantTypes {
let returnType = ConstantTypes.CAN_STRINGIFY
const props = getNodeProps(node)
if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
const { properties } = props
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i]
const keyType = getConstantType(key, context)
if (keyType === ConstantTypes.NOT_CONSTANT) {
return keyType
}
if (keyType < returnType) {
returnType = keyType
}
let valueType: ConstantTypes
if (value.type === NodeTypes.SIMPLE_EXPRESSION) {
valueType = getConstantType(value, context)
} else if (value.type === NodeTypes.JS_CALL_EXPRESSION) {
// some helper calls can be hoisted,
// such as the `normalizeProps` generated by the compiler for pre-normalize class,
// in this case we need to respect the ConstantType of the helper's arguments
valueType = getConstantTypeOfHelperCall(value, context)
} else {
valueType = ConstantTypes.NOT_CONSTANT
}
if (valueType === ConstantTypes.NOT_CONSTANT) {
return valueType
}
if (valueType < returnType) {
returnType = valueType
}
}
}
return returnType
}
function getNodeProps(node: PlainElementNode) {
const codegenNode = node.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
return codegenNode.props
}
}
function getPatchFlag(node: VNodeCall): number | undefined {
const flag = node.patchFlag
return flag ? parseInt(flag, 10) : undefined
}