diff --git a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap index 2b733c3f5a7..62402884778 100644 --- a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap @@ -3986,6 +3986,7 @@ Object { Object { "content": Object { "content": "a < b", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7151,6 +7152,7 @@ Object { Object { "content": Object { "content": "''", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7320,6 +7322,7 @@ Object { Object { "arg": Object { "content": "se", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7640,6 +7643,7 @@ Object { Object { "content": Object { "content": "", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7798,6 +7802,7 @@ Object { Object { "arg": Object { "content": "class", + "isConstant": true, "isStatic": true, "loc": Object { "end": Object { @@ -7816,6 +7821,7 @@ Object { }, "exp": Object { "content": "{ some: condition }", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7876,6 +7882,7 @@ Object { Object { "arg": Object { "content": "style", + "isConstant": true, "isStatic": true, "loc": Object { "end": Object { @@ -7894,6 +7901,7 @@ Object { }, "exp": Object { "content": "{ color: 'red' }", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -7983,6 +7991,7 @@ Object { Object { "arg": Object { "content": "style", + "isConstant": true, "isStatic": true, "loc": Object { "end": Object { @@ -8001,6 +8010,7 @@ Object { }, "exp": Object { "content": "{ color: 'red' }", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { @@ -8080,6 +8090,7 @@ Object { Object { "arg": Object { "content": "class", + "isConstant": true, "isStatic": true, "loc": Object { "end": Object { @@ -8098,6 +8109,7 @@ Object { }, "exp": Object { "content": "{ some: condition }", + "isConstant": false, "isStatic": false, "loc": Object { "end": Object { diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index e4dcf2b2b54..3e0ccef7986 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -298,6 +298,7 @@ describe('compiler: parse', () => { 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 }, @@ -322,6 +323,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: `a { type: NodeTypes.SIMPLE_EXPRESSION, content: `a { content: { type: NodeTypes.SIMPLE_EXPRESSION, isStatic: false, + isConstant: false, content: 'c>d', loc: { start: { offset: 12, line: 1, column: 13 }, @@ -390,6 +394,8 @@ describe('compiler: parse', () => { content: { type: NodeTypes.SIMPLE_EXPRESSION, isStatic: false, + // The `isConstant` is the default value and will be determined in `transformExpression`. + isConstant: false, content: '""', loc: { start: { offset: 8, line: 1, column: 9 }, @@ -974,6 +980,7 @@ describe('compiler: parse', () => { 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 }, @@ -999,6 +1006,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'click', isStatic: true, + isConstant: true, loc: { source: 'click', @@ -1071,6 +1079,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'click', isStatic: true, + isConstant: true, loc: { source: 'click', @@ -1107,6 +1116,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'a', isStatic: true, + isConstant: true, loc: { source: 'a', @@ -1127,6 +1137,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', isStatic: false, + isConstant: false, loc: { start: { offset: 8, line: 1, column: 9 }, @@ -1153,6 +1164,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'a', isStatic: true, + isConstant: true, loc: { source: 'a', @@ -1173,6 +1185,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', isStatic: false, + isConstant: false, loc: { start: { offset: 13, line: 1, column: 14 }, @@ -1199,6 +1212,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'a', isStatic: true, + isConstant: true, loc: { source: 'a', @@ -1219,6 +1233,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', isStatic: false, + isConstant: false, loc: { start: { offset: 8, line: 1, column: 9 }, @@ -1245,6 +1260,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'a', isStatic: true, + isConstant: true, loc: { source: 'a', @@ -1265,6 +1281,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', isStatic: false, + isConstant: false, loc: { start: { offset: 14, line: 1, column: 15 }, @@ -1291,6 +1308,7 @@ describe('compiler: parse', () => { type: NodeTypes.SIMPLE_EXPRESSION, content: 'a', isStatic: true, + isConstant: true, loc: { source: 'a', start: { @@ -1310,6 +1328,8 @@ describe('compiler: parse', () => { 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 }, diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap index bebf5aada1c..dba4bf1f095 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap @@ -152,6 +152,93 @@ return function render() { }" `; +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 diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts index 60ae861bf58..d998aa26cf6 100644 --- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -1,4 +1,10 @@ -import { parse, transform, NodeTypes, generate } from '../../src' +import { + parse, + transform, + NodeTypes, + generate, + CompilerOptions +} from '../../src' import { OPEN_BLOCK, CREATE_BLOCK, @@ -8,17 +14,24 @@ import { 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 } @@ -429,4 +442,186 @@ describe('compiler: hoistStatic transform', () => { }) expect(generate(root).code).toMatchSnapshot() }) + + describe('prefixIdentifiers', () => { + test('hoist nested static tree with static interpolation', () => { + const { root, args } = transformWithHoist( + `
foo {{ 1 }} {{ 2 }}
`, + { + 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( + `
{{ 1 }}
`, + { + 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( + `
{{ bar }}
`, + { + 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( + `

{{ o }}

`, + { + 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( + `

{{ o + 'foo' }}

`, + { + prefixIdentifiers: true + } + ) + + expect(root.hoists.length).toBe(0) + expect(generate(root).code).toMatchSnapshot() + }) + }) }) diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts index 417d0d18534..d93d321299b 100644 --- a/packages/compiler-core/__tests__/utils.spec.ts +++ b/packages/compiler-core/__tests__/utils.spec.ts @@ -79,6 +79,7 @@ describe('isEmptyExpression', () => { content: '', type: NodeTypes.SIMPLE_EXPRESSION, isStatic: true, + isConstant: true, loc: null as any }) ).toBe(true) @@ -90,6 +91,7 @@ describe('isEmptyExpression', () => { content: ' \t ', type: NodeTypes.SIMPLE_EXPRESSION, isStatic: true, + isConstant: true, loc: null as any }) ).toBe(true) @@ -101,6 +103,7 @@ describe('isEmptyExpression', () => { content: 'foo', type: NodeTypes.SIMPLE_EXPRESSION, isStatic: true, + isConstant: true, loc: null as any }) ).toBe(false) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index b6442202dfe..d866e950153 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -165,6 +165,7 @@ export interface SimpleExpressionNode extends Node { 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[] @@ -494,11 +495,13 @@ export function createObjectProperty( 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 } diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 1e8a6d2a81a..916f4925605 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -563,9 +563,12 @@ function parseAttribute( ) 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( @@ -581,6 +584,7 @@ function parseAttribute( type: NodeTypes.SIMPLE_EXPRESSION, content, isStatic, + isConstant, loc } } @@ -606,6 +610,8 @@ function parseAttribute( 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, @@ -712,6 +718,8 @@ function parseInterpolation( 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) }, diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index e74a2e7de34..5743d68136c 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -2,6 +2,7 @@ import { RootNode, NodeTypes, TemplateChildNode, + SimpleExpressionNode, ElementTypes, ElementCodegenNode, PlainElementNode, @@ -11,7 +12,7 @@ import { } 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) { @@ -107,7 +108,7 @@ function getPatchFlag(node: PlainElementNode): number | undefined { } function isStaticNode( - node: TemplateChildNode, + node: TemplateChildNode | SimpleExpressionNode, resultCache: Map ): boolean { switch (node.type) { @@ -119,7 +120,7 @@ function isStaticNode( 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)) { @@ -137,9 +138,17 @@ function isStaticNode( 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 diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 3df966a8f2c..66cbb1af966 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -183,7 +183,12 @@ export function buildProps( 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:

. + // Do not add prop `foo` to `dynamicPropNames`. + (!value.isStatic && !value.isConstant) + ) { const name = key.content if (name === 'ref') { hasRef = true diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index e3c2c028b80..083b3023cc7 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -61,6 +61,7 @@ export const transformExpression: NodeTransform = (node, context) => { interface PrefixMeta { prefix?: string + isConstant: boolean start: number end: number scopeIds?: Set @@ -108,6 +109,7 @@ export function processExpression( 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) { @@ -120,8 +122,15 @@ export function processExpression( 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) @@ -190,11 +199,16 @@ export function processExpression( } 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)) @@ -206,6 +220,7 @@ export function processExpression( ret = createCompoundExpression(children, node.loc) } else { ret = node + ret.isConstant = isConstant } ret.identifiers = Object.keys(knownIds) return ret diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts index 752d08c4ff5..6a5b22b4acc 100644 --- a/packages/compiler-dom/__tests__/parse.spec.ts +++ b/packages/compiler-dom/__tests__/parse.spec.ts @@ -117,6 +117,7 @@ describe('DOM parser', () => { 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 },