Object {
"content": Object {
"content": "a < b",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"content": Object {
"content": "'</div>'",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "se",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"content": Object {
"content": "",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "class",
+ "isConstant": true,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ some: condition }",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "style",
+ "isConstant": true,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ color: 'red' }",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "style",
+ "isConstant": true,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ color: 'red' }",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "class",
+ "isConstant": true,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ some: condition }",
+ "isConstant": false,
"isStatic": false,
"loc": Object {
"end": Object {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `message`,
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 2, line: 1, column: 3 },
end: { offset: 9, line: 1, column: 10 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a<b`,
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 3, line: 1, column: 4 },
end: { offset: 6, line: 1, column: 7 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a<b`,
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 3, line: 1, column: 4 },
end: { offset: 6, line: 1, column: 7 },
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
+ isConstant: false,
content: 'c>d',
loc: {
start: { offset: 12, line: 1, column: 13 },
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
+ // The `isConstant` is the default value and will be determined in `transformExpression`.
+ isConstant: false,
content: '"</div>"',
loc: {
start: { offset: 8, line: 1, column: 9 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 11, line: 1, column: 12 },
end: { offset: 12, line: 1, column: 13 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'click',
isStatic: true,
+ isConstant: true,
loc: {
source: 'click',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'click',
isStatic: true,
+ isConstant: true,
loc: {
source: 'click',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
+ isConstant: true,
loc: {
source: 'a',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
+ isConstant: true,
loc: {
source: 'a',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 13, line: 1, column: 14 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
+ isConstant: true,
loc: {
source: 'a',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
+ isConstant: true,
loc: {
source: 'a',
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 14, line: 1, column: 15 },
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
+ isConstant: true,
loc: {
source: 'a',
start: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: '{ b }',
isStatic: false,
+ // The `isConstant` is the default value and will be determined in transformExpression
+ isConstant: false,
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 15, line: 1, column: 16 },
}"
`;
+exports[`compiler: hoistStatic transform prefixIdentifiers hoist class with static object value 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = { class: { foo: true }}
+
+return function render() {
+ with (this) {
+ const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"div\\", null, [
+ _createVNode(\\"span\\", _hoisted_1, _toString(_ctx.bar), 1 /* TEXT */)
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static interpolation 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = _createVNode(\\"span\\", null, [\\"foo \\", _toString(1), _toString(2)])
+
+return function render() {
+ with (this) {
+ const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"div\\", null, [
+ _hoisted_1
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static tree with static prop value 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toString(1), 1 /* TEXT */)
+
+return function render() {
+ with (this) {
+ const { toString: _toString, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+
+ return (_openBlock(), _createBlock(\\"div\\", null, [
+ _hoisted_1
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that with scope variable (2) 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue
+
+ return (_openBlock(), _createBlock(\\"div\\", null, [
+ (_openBlock(), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
+ return (_openBlock(), _createBlock(\\"p\\", null, [
+ _createVNode(\\"span\\", null, _toString(o + 'foo'), 1 /* TEXT */)
+ ]))
+ }), 128 /* UNKEYED_FRAGMENT */))
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that with scope variable 1`] = `
+"const _Vue = Vue
+
+return function render() {
+ with (this) {
+ const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, toString: _toString, createVNode: _createVNode } = _Vue
+
+ return (_openBlock(), _createBlock(\\"div\\", null, [
+ (_openBlock(), _createBlock(_Fragment, null, _renderList(_ctx.list, (o) => {
+ return (_openBlock(), _createBlock(\\"p\\", null, [
+ _createVNode(\\"span\\", null, _toString(o), 1 /* TEXT */)
+ ]))
+ }), 128 /* UNKEYED_FRAGMENT */))
+ ]))
+ }
+}"
+`;
+
exports[`compiler: hoistStatic transform should NOT hoist components 1`] = `
"const _Vue = Vue
-import { parse, transform, NodeTypes, generate } from '../../src'
+import {
+ parse,
+ transform,
+ NodeTypes,
+ generate,
+ CompilerOptions
+} from '../../src'
import {
OPEN_BLOCK,
CREATE_BLOCK,
RENDER_LIST
} from '../../src/runtimeHelpers'
import { transformElement } from '../../src/transforms/transformElement'
+import { transformExpression } from '../../src/transforms/transformExpression'
import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { PatchFlags } from '@vue/shared'
-function transformWithHoist(template: string) {
+function transformWithHoist(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
hoistStatic: true,
- nodeTransforms: [transformIf, transformFor, transformElement],
+ prefixIdentifiers: options.prefixIdentifiers,
+ nodeTransforms: [
+ transformIf,
+ transformFor,
+ ...(options.prefixIdentifiers ? [transformExpression] : []),
+ transformElement
+ ],
directiveTransforms: {
bind: transformBind
}
})
expect(generate(root).code).toMatchSnapshot()
})
+
+ describe('prefixIdentifiers', () => {
+ test('hoist nested static tree with static interpolation', () => {
+ const { root, args } = transformWithHoist(
+ `<div><span>foo {{ 1 }} {{ 2 }}</span></div>`,
+ {
+ prefixIdentifiers: true
+ }
+ )
+ expect(root.hoists).toMatchObject([
+ {
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: CREATE_VNODE,
+ arguments: [
+ `"span"`,
+ `null`,
+ [
+ {
+ type: NodeTypes.TEXT,
+ content: `foo `
+ },
+ {
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ content: `1`,
+ isStatic: false,
+ isConstant: true
+ }
+ },
+ {
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ content: `2`,
+ isStatic: false,
+ isConstant: true
+ }
+ }
+ ]
+ ]
+ }
+ ])
+ expect(args).toMatchObject([
+ `"div"`,
+ `null`,
+ [
+ {
+ type: NodeTypes.ELEMENT,
+ codegenNode: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `_hoisted_1`
+ }
+ }
+ ]
+ ])
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('hoist nested static tree with static prop value', () => {
+ const { root, args } = transformWithHoist(
+ `<div><span :foo="0">{{ 1 }}</span></div>`,
+ {
+ prefixIdentifiers: true
+ }
+ )
+
+ expect(root.hoists).toMatchObject([
+ {
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: CREATE_VNODE,
+ arguments: [
+ `"span"`,
+ createObjectMatcher({ foo: `[0]` }),
+ {
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ content: `1`,
+ isStatic: false,
+ isConstant: true
+ }
+ },
+ '1 /* TEXT */'
+ ]
+ }
+ ])
+ expect(args).toMatchObject([
+ `"div"`,
+ `null`,
+ [
+ {
+ type: NodeTypes.ELEMENT,
+ codegenNode: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `_hoisted_1`
+ }
+ }
+ ]
+ ])
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('hoist class with static object value', () => {
+ const { root, args } = transformWithHoist(
+ `<div><span :class="{ foo: true }">{{ bar }}</span></div>`,
+ {
+ prefixIdentifiers: true
+ }
+ )
+
+ expect(root.hoists).toMatchObject([
+ {
+ type: NodeTypes.JS_OBJECT_EXPRESSION,
+ properties: [
+ {
+ key: {
+ content: `class`,
+ isConstant: true,
+ isStatic: true
+ },
+ value: {
+ content: `{ foo: true }`,
+ isConstant: true,
+ isStatic: false
+ }
+ }
+ ]
+ }
+ ])
+ expect(args).toMatchObject([
+ `"div"`,
+ `null`,
+ [
+ {
+ type: NodeTypes.ELEMENT,
+ codegenNode: {
+ callee: CREATE_VNODE,
+ arguments: [
+ `"span"`,
+ {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `_hoisted_1`
+ },
+ {
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ content: `_ctx.bar`,
+ isConstant: false,
+ isStatic: false
+ }
+ },
+ `1 /* TEXT */`
+ ]
+ }
+ }
+ ]
+ ])
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('should NOT hoist expressions that with scope variable', () => {
+ const { root } = transformWithHoist(
+ `<div><p v-for="o in list"><span>{{ o }}</span></p></div>`,
+ {
+ prefixIdentifiers: true
+ }
+ )
+
+ expect(root.hoists.length).toBe(0)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('should NOT hoist expressions that with scope variable (2)', () => {
+ const { root } = transformWithHoist(
+ `<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`,
+ {
+ prefixIdentifiers: true
+ }
+ )
+
+ expect(root.hoists.length).toBe(0)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+ })
})
content: '',
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: true,
+ isConstant: true,
loc: null as any
})
).toBe(true)
content: ' \t ',
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: true,
+ isConstant: true,
loc: null as any
})
).toBe(true)
content: 'foo',
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: true,
+ isConstant: true,
loc: null as any
})
).toBe(false)
type: NodeTypes.SIMPLE_EXPRESSION
content: string
isStatic: boolean
+ isConstant: boolean
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
export function createSimpleExpression(
content: SimpleExpressionNode['content'],
isStatic: SimpleExpressionNode['isStatic'],
- loc: SourceLocation = locStub
+ loc: SourceLocation = locStub,
+ isConstant: boolean = false
): SimpleExpressionNode {
return {
type: NodeTypes.SIMPLE_EXPRESSION,
loc,
+ isConstant,
content,
isStatic
}
)
let content = match[2]
let isStatic = true
+ // Non-dynamic arg is a constant.
+ let isConstant = true
if (content.startsWith('[')) {
isStatic = false
+ isConstant = false
if (!content.endsWith(']')) {
emitError(
type: NodeTypes.SIMPLE_EXPRESSION,
content,
isStatic,
+ isConstant,
loc
}
}
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
+ // Set `isConstant` to false by default and will decide in transformExpression
+ isConstant: false,
loc: value.loc
},
arg,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
+ // Set `isConstant` to false by default and will decide in transformExpression
+ isConstant: false,
content,
loc: getSelection(context, innerStart, innerEnd)
},
RootNode,
NodeTypes,
TemplateChildNode,
+ SimpleExpressionNode,
ElementTypes,
ElementCodegenNode,
PlainElementNode,
} from '../ast'
import { TransformContext } from '../transform'
import { APPLY_DIRECTIVES } from '../runtimeHelpers'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet, findProp } from '../utils'
function hasDynamicKey(node: ElementNode) {
}
function isStaticNode(
- node: TemplateChildNode,
+ node: TemplateChildNode | SimpleExpressionNode,
resultCache: Map<TemplateChildNode, boolean>
): boolean {
switch (node.type) {
return resultCache.get(node) as boolean
}
const flag = getPatchFlag(node)
- if (!flag) {
+ if (!flag || flag === PatchFlags.TEXT) {
// element self is static. check its children.
for (let i = 0; i < node.children.length; i++) {
if (!isStaticNode(node.children[i], resultCache)) {
return true
case NodeTypes.IF:
case NodeTypes.FOR:
+ return false
case NodeTypes.INTERPOLATION:
+ return isStaticNode(node.content, resultCache)
+ case NodeTypes.SIMPLE_EXPRESSION:
+ return node.isConstant
case NodeTypes.COMPOUND_EXPRESSION:
- return false
+ return node.children.every(child => {
+ return (
+ isString(child) || isSymbol(child) || isStaticNode(child, resultCache)
+ )
+ })
default:
if (__DEV__) {
const exhaustiveCheck: never = node
const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
- if (value.type !== NodeTypes.SIMPLE_EXPRESSION || !value.isStatic) {
+ if (
+ value.type !== NodeTypes.SIMPLE_EXPRESSION ||
+ // E.g: <p :foo="1 + 2" />.
+ // Do not add prop `foo` to `dynamicPropNames`.
+ (!value.isStatic && !value.isConstant)
+ ) {
const name = key.content
if (name === 'ref') {
hasRef = true
interface PrefixMeta {
prefix?: string
+ isConstant: boolean
start: number
end: number
scopeIds?: Set<string>
const ids: (Identifier & PrefixMeta)[] = []
const knownIds = Object.create(context.identifiers)
+ let isConstant = true
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
walkJS(ast, {
enter(node: Node & PrefixMeta, parent) {
node.prefix = `${node.name}: `
}
node.name = `_ctx.${node.name}`
+ node.isConstant = false
+ isConstant = false
ids.push(node)
} else if (!isStaticPropertyKey(node, parent)) {
+ // This means this identifier is pointing to a scope variable (a v-for alias, or a v-slot prop)
+ // which is also dynamic and cannot be hoisted.
+ node.isConstant = !(
+ knownIds[node.name] && shouldPrefix(node, parent)
+ )
// also generate sub-expressions for other identifiers for better
// source map support. (except for property keys which are static)
ids.push(node)
}
const source = rawExp.slice(start, end)
children.push(
- createSimpleExpression(id.name, false, {
- source,
- start: advancePositionWithClone(node.loc.start, source, start),
- end: advancePositionWithClone(node.loc.start, source, end)
- })
+ createSimpleExpression(
+ id.name,
+ false,
+ {
+ source,
+ start: advancePositionWithClone(node.loc.start, source, start),
+ end: advancePositionWithClone(node.loc.start, source, end)
+ },
+ id.isConstant /* isConstant */
+ )
)
if (i === ids.length - 1 && end < rawExp.length) {
children.push(rawExp.slice(end))
ret = createCompoundExpression(children, node.loc)
} else {
ret = node
+ ret.isConstant = isConstant
}
ret.identifiers = Object.keys(knownIds)
return ret
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a < b`,
isStatic: false,
+ isConstant: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 16, line: 1, column: 17 },