275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
import {
|
|
RootNode,
|
|
NodeTypes,
|
|
TemplateChildNode,
|
|
SimpleExpressionNode,
|
|
ElementTypes,
|
|
PlainElementNode,
|
|
ComponentNode,
|
|
TemplateNode,
|
|
VNodeCall,
|
|
ParentNode
|
|
} from '../ast'
|
|
import { TransformContext } from '../transform'
|
|
import { PatchFlags, isString, isSymbol } from '@vue/shared'
|
|
import { isSlotOutlet } from '../utils'
|
|
|
|
export function hoistStatic(root: RootNode, context: TransformContext) {
|
|
walk(
|
|
root,
|
|
context,
|
|
new Map(),
|
|
// 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)
|
|
)
|
|
}
|
|
|
|
const enum StaticType {
|
|
NOT_STATIC = 0,
|
|
FULL_STATIC,
|
|
HAS_RUNTIME_CONSTANT
|
|
}
|
|
|
|
function walk(
|
|
node: ParentNode,
|
|
context: TransformContext,
|
|
resultCache: Map<TemplateChildNode, StaticType>,
|
|
doNotHoistNode: boolean = false
|
|
) {
|
|
let hasHoistedNode = false
|
|
// Some transforms, e.g. transformAssetUrls from @vue/compiler-sfc, replaces
|
|
// static bindings with expressions. These expressions are guaranteed to be
|
|
// constant so they are still eligible for hoisting, but they are only
|
|
// available at runtime and therefore cannot be evaluated ahead of time.
|
|
// This is only a concern for pre-stringification (via transformHoist by
|
|
// @vue/compiler-dom), but doing it here allows us to perform only one full
|
|
// walk of the AST and allow `stringifyStatic` to stop walking as soon as its
|
|
// stringficiation threshold is met.
|
|
let hasRuntimeConstant = false
|
|
|
|
const { children } = node
|
|
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
|
|
) {
|
|
let staticType
|
|
if (
|
|
!doNotHoistNode &&
|
|
(staticType = getStaticType(child, resultCache)) > 0
|
|
) {
|
|
if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
|
|
hasRuntimeConstant = true
|
|
}
|
|
// whole tree is static
|
|
;(child.codegenNode as VNodeCall).patchFlag =
|
|
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
|
|
child.codegenNode = context.hoist(child.codegenNode!)
|
|
hasHoistedNode = true
|
|
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) &&
|
|
!hasNonHoistableProps(child)
|
|
) {
|
|
const props = getNodeProps(child)
|
|
if (props) {
|
|
codegenNode.props = context.hoist(props)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (child.type === NodeTypes.TEXT_CALL) {
|
|
const staticType = getStaticType(child.content, resultCache)
|
|
if (staticType > 0) {
|
|
if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
|
|
hasRuntimeConstant = true
|
|
}
|
|
child.codegenNode = context.hoist(child.codegenNode)
|
|
hasHoistedNode = true
|
|
}
|
|
}
|
|
|
|
// walk further
|
|
if (child.type === NodeTypes.ELEMENT) {
|
|
walk(child, context, resultCache)
|
|
} else if (child.type === NodeTypes.FOR) {
|
|
// Do not hoist v-for single child because it has to be a block
|
|
walk(child, context, resultCache, 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,
|
|
resultCache,
|
|
child.branches[i].children.length === 1
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {
|
|
context.transformHoist(children, context, node)
|
|
}
|
|
}
|
|
|
|
export function getStaticType(
|
|
node: TemplateChildNode | SimpleExpressionNode,
|
|
resultCache: Map<TemplateChildNode, StaticType> = new Map()
|
|
): StaticType {
|
|
switch (node.type) {
|
|
case NodeTypes.ELEMENT:
|
|
if (node.tagType !== ElementTypes.ELEMENT) {
|
|
return StaticType.NOT_STATIC
|
|
}
|
|
const cached = resultCache.get(node)
|
|
if (cached !== undefined) {
|
|
return cached
|
|
}
|
|
const codegenNode = node.codegenNode!
|
|
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
|
|
return StaticType.NOT_STATIC
|
|
}
|
|
const flag = getPatchFlag(codegenNode)
|
|
if (!flag && !hasNonHoistableProps(node)) {
|
|
// element self is static. check its children.
|
|
let returnType = StaticType.FULL_STATIC
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
const childType = getStaticType(node.children[i], resultCache)
|
|
if (childType === StaticType.NOT_STATIC) {
|
|
resultCache.set(node, StaticType.NOT_STATIC)
|
|
return StaticType.NOT_STATIC
|
|
} else if (childType === StaticType.HAS_RUNTIME_CONSTANT) {
|
|
returnType = StaticType.HAS_RUNTIME_CONSTANT
|
|
}
|
|
}
|
|
|
|
// check if any of the props contain runtime constants
|
|
if (returnType !== StaticType.HAS_RUNTIME_CONSTANT) {
|
|
for (let i = 0; i < node.props.length; i++) {
|
|
const p = node.props[i]
|
|
if (
|
|
p.type === NodeTypes.DIRECTIVE &&
|
|
p.name === 'bind' &&
|
|
p.exp &&
|
|
(p.exp.type === NodeTypes.COMPOUND_EXPRESSION ||
|
|
p.exp.isRuntimeConstant)
|
|
) {
|
|
returnType = StaticType.HAS_RUNTIME_CONSTANT
|
|
}
|
|
}
|
|
}
|
|
|
|
// only svg/foreignObject could be block here, however if they are
|
|
// stati then they don't need to be blocks since there will be no
|
|
// nested updates.
|
|
if (codegenNode.isBlock) {
|
|
codegenNode.isBlock = false
|
|
}
|
|
|
|
resultCache.set(node, returnType)
|
|
return returnType
|
|
} else {
|
|
resultCache.set(node, StaticType.NOT_STATIC)
|
|
return StaticType.NOT_STATIC
|
|
}
|
|
case NodeTypes.TEXT:
|
|
case NodeTypes.COMMENT:
|
|
return StaticType.FULL_STATIC
|
|
case NodeTypes.IF:
|
|
case NodeTypes.FOR:
|
|
case NodeTypes.IF_BRANCH:
|
|
return StaticType.NOT_STATIC
|
|
case NodeTypes.INTERPOLATION:
|
|
case NodeTypes.TEXT_CALL:
|
|
return getStaticType(node.content, resultCache)
|
|
case NodeTypes.SIMPLE_EXPRESSION:
|
|
return node.isConstant
|
|
? node.isRuntimeConstant
|
|
? StaticType.HAS_RUNTIME_CONSTANT
|
|
: StaticType.FULL_STATIC
|
|
: StaticType.NOT_STATIC
|
|
case NodeTypes.COMPOUND_EXPRESSION:
|
|
let returnType = StaticType.FULL_STATIC
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
const child = node.children[i]
|
|
if (isString(child) || isSymbol(child)) {
|
|
continue
|
|
}
|
|
const childType = getStaticType(child, resultCache)
|
|
if (childType === StaticType.NOT_STATIC) {
|
|
return StaticType.NOT_STATIC
|
|
} else if (childType === StaticType.HAS_RUNTIME_CONSTANT) {
|
|
returnType = StaticType.HAS_RUNTIME_CONSTANT
|
|
}
|
|
}
|
|
return returnType
|
|
default:
|
|
if (__DEV__) {
|
|
const exhaustiveCheck: never = node
|
|
exhaustiveCheck
|
|
}
|
|
return StaticType.NOT_STATIC
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function hasNonHoistableProps(node: PlainElementNode): boolean {
|
|
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]
|
|
if (
|
|
key.type !== NodeTypes.SIMPLE_EXPRESSION ||
|
|
!key.isStatic ||
|
|
(value.type !== NodeTypes.SIMPLE_EXPRESSION ||
|
|
(!value.isStatic && !value.isConstant))
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|