fix(compiler-dom): should bail stringification on runtime constant regardless of position

ref: vuejs/vite#157
This commit is contained in:
Evan You 2020-05-18 10:04:03 -04:00
parent 45c96a06aa
commit dd2bfb5a8f
4 changed files with 99 additions and 49 deletions

View File

@ -37,13 +37,29 @@ export function isSingleElementRoot(
) )
} }
const enum StaticType {
NOT_STATIC = 0,
FULL_STATIC,
HAS_RUNTIME_CONSTANT
}
function walk( function walk(
children: TemplateChildNode[], children: TemplateChildNode[],
context: TransformContext, context: TransformContext,
resultCache: Map<TemplateChildNode, boolean>, resultCache: Map<TemplateChildNode, StaticType>,
doNotHoistNode: boolean = false doNotHoistNode: boolean = false
) { ) {
let hasHoistedNode = false let hasHoistedNode = false
// Some transforms, e.g. trasnformAssetUrls 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
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
// only plain elements & text calls are eligible for hoisting. // only plain elements & text calls are eligible for hoisting.
@ -51,7 +67,14 @@ function walk(
child.type === NodeTypes.ELEMENT && child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT child.tagType === ElementTypes.ELEMENT
) { ) {
if (!doNotHoistNode && isStaticNode(child, resultCache)) { let staticType
if (
!doNotHoistNode &&
(staticType = getStaticType(child, resultCache)) > 0
) {
if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
hasRuntimeConstant = true
}
// whole tree is static // whole tree is static
;(child.codegenNode as VNodeCall).patchFlag = ;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
@ -78,12 +101,15 @@ function walk(
} }
} }
} }
} else if ( } else if (child.type === NodeTypes.TEXT_CALL) {
child.type === NodeTypes.TEXT_CALL && const staticType = getStaticType(child.content, resultCache)
isStaticNode(child.content, resultCache) if (staticType > 0) {
) { if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
child.codegenNode = context.hoist(child.codegenNode) hasRuntimeConstant = true
hasHoistedNode = true }
child.codegenNode = context.hoist(child.codegenNode)
hasHoistedNode = true
}
} }
// walk further // walk further
@ -101,19 +127,19 @@ function walk(
} }
} }
if (hasHoistedNode && context.transformHoist) { if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {
context.transformHoist(children, context) context.transformHoist(children, context)
} }
} }
export function isStaticNode( export function getStaticType(
node: TemplateChildNode | SimpleExpressionNode, node: TemplateChildNode | SimpleExpressionNode,
resultCache: Map<TemplateChildNode, boolean> = new Map() resultCache: Map<TemplateChildNode, StaticType> = new Map()
): boolean { ): StaticType {
switch (node.type) { switch (node.type) {
case NodeTypes.ELEMENT: case NodeTypes.ELEMENT:
if (node.tagType !== ElementTypes.ELEMENT) { if (node.tagType !== ElementTypes.ELEMENT) {
return false return StaticType.NOT_STATIC
} }
const cached = resultCache.get(node) const cached = resultCache.get(node)
if (cached !== undefined) { if (cached !== undefined) {
@ -121,53 +147,88 @@ export function isStaticNode(
} }
const codegenNode = node.codegenNode! const codegenNode = node.codegenNode!
if (codegenNode.type !== NodeTypes.VNODE_CALL) { if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return false return StaticType.NOT_STATIC
} }
const flag = getPatchFlag(codegenNode) const flag = getPatchFlag(codegenNode)
if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) { if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) {
// element self is static. check its children. // element self is static. check its children.
let returnType = StaticType.FULL_STATIC
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {
if (!isStaticNode(node.children[i], resultCache)) { const childType = getStaticType(node.children[i], resultCache)
resultCache.set(node, false) if (childType === StaticType.NOT_STATIC) {
return false resultCache.set(node, StaticType.NOT_STATIC)
return StaticType.NOT_STATIC
} else if (childType === StaticType.HAS_RUNTIME_CONSTANT) {
returnType = StaticType.HAS_RUNTIME_CONSTANT
} }
} }
// 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 // check if any of the props contain runtime constants
// updates. 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) { if (codegenNode.isBlock) {
codegenNode.isBlock = false codegenNode.isBlock = false
} }
resultCache.set(node, true)
return true resultCache.set(node, returnType)
return returnType
} else { } else {
resultCache.set(node, false) resultCache.set(node, StaticType.NOT_STATIC)
return false return StaticType.NOT_STATIC
} }
case NodeTypes.TEXT: case NodeTypes.TEXT:
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
return true return StaticType.FULL_STATIC
case NodeTypes.IF: case NodeTypes.IF:
case NodeTypes.FOR: case NodeTypes.FOR:
case NodeTypes.IF_BRANCH: case NodeTypes.IF_BRANCH:
return false return StaticType.NOT_STATIC
case NodeTypes.INTERPOLATION: case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL: case NodeTypes.TEXT_CALL:
return isStaticNode(node.content, resultCache) return getStaticType(node.content, resultCache)
case NodeTypes.SIMPLE_EXPRESSION: case NodeTypes.SIMPLE_EXPRESSION:
return node.isConstant return node.isConstant
? node.isRuntimeConstant
? StaticType.HAS_RUNTIME_CONSTANT
: StaticType.FULL_STATIC
: StaticType.NOT_STATIC
case NodeTypes.COMPOUND_EXPRESSION: case NodeTypes.COMPOUND_EXPRESSION:
return node.children.every(child => { let returnType = StaticType.FULL_STATIC
return ( for (let i = 0; i < node.children.length; i++) {
isString(child) || isSymbol(child) || isStaticNode(child, resultCache) 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: default:
if (__DEV__) { if (__DEV__) {
const exhaustiveCheck: never = node const exhaustiveCheck: never = node
exhaustiveCheck exhaustiveCheck
} }
return false return StaticType.NOT_STATIC
} }
} }

View File

@ -46,7 +46,7 @@ import {
findDir findDir
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { isStaticNode } from './hoistStatic' import { getStaticType } from './hoistStatic'
// some directive transforms (e.g. v-model) may return a symbol for runtime // some directive transforms (e.g. v-model) may return a symbol for runtime
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
@ -156,7 +156,7 @@ export const transformElement: NodeTransform = (node, context) => {
const hasDynamicTextChild = const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION || type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION type === NodeTypes.COMPOUND_EXPRESSION
if (hasDynamicTextChild && !isStaticNode(child)) { if (hasDynamicTextChild && !getStaticType(child)) {
patchFlag |= PatchFlags.TEXT patchFlag |= PatchFlags.TEXT
} }
// pass directly if the only child is a text node // pass directly if the only child is a text node
@ -292,7 +292,7 @@ export function buildProps(
value.type === NodeTypes.JS_CACHE_EXPRESSION || value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION || ((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) && value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value)) getStaticType(value) > 0)
) { ) {
// skip if the prop is a cached handler or has constant value // skip if the prop is a cached handler or has constant value
return return

View File

@ -161,10 +161,10 @@ describe('stringify static html', () => {
test('should bail on runtime constant v-bind bindings', () => { test('should bail on runtime constant v-bind bindings', () => {
const { ast } = compile( const { ast } = compile(
`<div><div><img src="./foo" />${repeat( `<div><div>${repeat(
`<span class="foo">foo</span>`, `<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
)}</div></div>`, )}<img src="./foo" /></div></div>`,
{ {
hoistStatic: true, hoistStatic: true,
prefixIdentifiers: true, prefixIdentifiers: true,

View File

@ -185,17 +185,6 @@ function analyzeNode(node: StringiableNode): [number, number] | false {
) { ) {
return bail() return bail()
} }
// some transforms, e.g. `transformAssetUrls` in `@vue/compiler-sfc` may
// convert static attributes into a v-bind with a constnat expresion.
// Such constant bindings are eligible for hoisting but not for static
// stringification because they cannot be pre-evaluated.
if (
p.exp &&
(p.exp.type === NodeTypes.COMPOUND_EXPRESSION ||
p.exp.isRuntimeConstant)
) {
return bail()
}
} }
} }
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {