diff --git a/packages/compiler-core/__tests__/testUtils.ts b/packages/compiler-core/__tests__/testUtils.ts index 16bd554e2b7..74855d5183a 100644 --- a/packages/compiler-core/__tests__/testUtils.ts +++ b/packages/compiler-core/__tests__/testUtils.ts @@ -7,7 +7,7 @@ import { ElementCodegenNode } from '../src' import { CREATE_VNODE } from '../src/runtimeHelpers' -import { isString } from '@vue/shared' +import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared' const leadingBracketRE = /^\[/ const bracketsRE = /^\[|\]$/g @@ -58,3 +58,15 @@ export function createElementWithCodegen( } } } + +export function genFlagText(flag: PatchFlags | PatchFlags[]) { + if (isArray(flag)) { + let f = 0 + flag.forEach(ff => { + f |= ff + }) + return `${f} /* ${flag.map(f => PatchFlagNames[f]).join(', ')} */` + } else { + return `${flag} /* ${PatchFlagNames[flag]} */` + } +} diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap new file mode 100644 index 00000000000..5ca448b03c0 --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`compiler: hositStatic transform hoist nested static tree 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = _createVNode(\\"p\\", null, [ + _createVNode(\\"span\\"), + _createVNode(\\"span\\") +]) + +return function render() { + with (this) { + const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _hoisted_1 + ])) + } +}" +`; + +exports[`compiler: hositStatic transform hoist siblings with common non-hoistable parent 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = _createVNode(\\"span\\") +const _hoisted_2 = _createVNode(\\"div\\") + +return function render() { + with (this) { + const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _hoisted_1, + _hoisted_2 + ])) + } +}" +`; + +exports[`compiler: hositStatic transform hoist simple element 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\") + +return function render() { + with (this) { + const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _hoisted_1 + ])) + } +}" +`; + +exports[`compiler: hositStatic transform hoist static props for elements with directives 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = { id: \\"foo\\" } + +return function render() { + with (this) { + const { createVNode: _createVNode, applyDirectives: _applyDirectives, resolveDirective: _resolveDirective, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _directive_foo = _resolveDirective(\\"foo\\") + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _applyDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [ + [_directive_foo] + ]) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform hoist static props for elements with dynamic text children 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = { id: \\"foo\\" } + +return function render() { + with (this) { + const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _createVNode(\\"div\\", _hoisted_1, _toString(hello), 1 /* TEXT */) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform hoist static props for elements with unhoistable children 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = { id: \\"foo\\" } + +return function render() { + with (this) { + const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _createVNode(\\"div\\", _hoisted_1, [ + _createVNode(_component_Comp) + ]) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform should NOT hoist components 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _createVNode(_component_Comp) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform should NOT hoist element with dynamic props 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform should NOT hoist root node 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return (_openBlock(), _createBlock(\\"div\\")) + } +}" +`; + +exports[`compiler: hositStatic transform should hoist v-for children if static 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = { id: \\"foo\\" } +const _hoisted_2 = _createVNode(\\"span\\") + +return function render() { + with (this) { + const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + (_openBlock(), _createBlock(_Fragment, null, _renderList(list, (i) => { + return (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [ + _hoisted_2 + ])) + }), 128 /* UNKEYED_FRAGMENT */)) + ])) + } +}" +`; + +exports[`compiler: hositStatic transform should hoist v-if props/children if static 1`] = ` +"const _Vue = Vue +const _createVNode = Vue.createVNode + +const _hoisted_1 = { + key: 0, + id: \\"foo\\" +} +const _hoisted_2 = _createVNode(\\"span\\") + +return function render() { + with (this) { + const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, Empty: _Empty } = _Vue + + return (_openBlock(), _createBlock(\\"div\\", null, [ + (_openBlock(), ok + ? _createBlock(\\"div\\", _hoisted_1, [ + _hoisted_2 + ]) + : _createBlock(_Empty)) + ])) + } +}" +`; diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts new file mode 100644 index 00000000000..7e5e201037a --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -0,0 +1,359 @@ +import { parse, transform, NodeTypes, generate } from '../../src' +import { + OPEN_BLOCK, + CREATE_BLOCK, + CREATE_VNODE, + APPLY_DIRECTIVES, + FRAGMENT, + RENDER_LIST +} from '../../src/runtimeHelpers' +import { transformElement } from '../../src/transforms/transformElement' +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) { + const ast = parse(template) + transform(ast, { + hoistStatic: true, + nodeTransforms: [transformIf, transformFor, transformElement], + directiveTransforms: { + bind: transformBind + } + }) + expect(ast.codegenNode).toMatchObject({ + type: NodeTypes.JS_SEQUENCE_EXPRESSION, + expressions: [ + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: OPEN_BLOCK + }, + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_BLOCK + } + ] + }) + return { + root: ast, + args: (ast.codegenNode as any).expressions[1].arguments + } +} + +describe('compiler: hositStatic transform', () => { + test('should NOT hoist root node', () => { + // if the whole tree is static, the root still needs to be a block + // so that it's patched in optimized mode to skip children + const { root, args } = transformWithHoist(`
`) + expect(root.hoists.length).toBe(0) + expect(args).toEqual([`"div"`]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('hoist simple element', () => { + const { root, args } = transformWithHoist( + `
hello
` + ) + expect(root.hoists).toMatchObject([ + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [ + `"span"`, + createObjectMatcher({ class: 'inline' }), + { + type: NodeTypes.TEXT, + content: `hello` + } + ] + } + ]) + 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', () => { + const { root, args } = transformWithHoist( + `

` + ) + expect(root.hoists).toMatchObject([ + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [ + `"p"`, + `null`, + [ + { type: NodeTypes.ELEMENT, tag: `span` }, + { type: NodeTypes.ELEMENT, tag: `span` } + ] + ] + } + ]) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `_hoisted_1` + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('hoist siblings with common non-hoistable parent', () => { + const { root, args } = transformWithHoist(`
`) + expect(root.hoists).toMatchObject([ + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [`"span"`] + }, + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE, + arguments: [`"div"`] + } + ]) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `_hoisted_1` + } + }, + { + type: NodeTypes.ELEMENT, + codegenNode: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `_hoisted_2` + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('should NOT hoist components', () => { + const { root, args } = transformWithHoist(`
`) + expect(root.hoists.length).toBe(0) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + callee: CREATE_VNODE, + arguments: [`_component_Comp`] + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('should NOT hoist element with dynamic props', () => { + const { root, args } = transformWithHoist(`
`) + expect(root.hoists.length).toBe(0) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + callee: CREATE_VNODE, + arguments: [ + `"div"`, + createObjectMatcher({ + id: `[foo]` + }), + `null`, + genFlagText(PatchFlags.PROPS), + `["id"]` + ] + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('hoist static props for elements with directives', () => { + const { root, args } = transformWithHoist( + `
` + ) + expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + callee: APPLY_DIRECTIVES, + arguments: [ + { + callee: CREATE_VNODE, + arguments: [ + `"div"`, + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `_hoisted_1` + }, + `null`, + genFlagText(PatchFlags.NEED_PATCH) + ] + }, + { + type: NodeTypes.JS_ARRAY_EXPRESSION + } + ] + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('hoist static props for elements with dynamic text children', () => { + const { root, args } = transformWithHoist( + `
{{ hello }}
` + ) + expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + callee: CREATE_VNODE, + arguments: [ + `"div"`, + { content: `_hoisted_1` }, + { type: NodeTypes.INTERPOLATION }, + genFlagText(PatchFlags.TEXT) + ] + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('hoist static props for elements with unhoistable children', () => { + const { root, args } = transformWithHoist( + `
` + ) + expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) + expect(args[2]).toMatchObject([ + { + type: NodeTypes.ELEMENT, + codegenNode: { + callee: CREATE_VNODE, + arguments: [ + `"div"`, + { content: `_hoisted_1` }, + [{ type: NodeTypes.ELEMENT, tag: `Comp` }] + ] + } + } + ]) + expect(generate(root).code).toMatchSnapshot() + }) + + test('should hoist v-if props/children if static', () => { + const { root, args } = transformWithHoist( + `
` + ) + expect(root.hoists).toMatchObject([ + createObjectMatcher({ + key: `[0]`, // key injected by v-if branch + id: 'foo' + }), + { + callee: CREATE_VNODE, + arguments: [`"span"`] + } + ]) + expect(args[2][0].codegenNode).toMatchObject({ + type: NodeTypes.JS_SEQUENCE_EXPRESSION, + expressions: [ + { callee: OPEN_BLOCK }, + { + type: NodeTypes.JS_CONDITIONAL_EXPRESSION, + consequent: { + // blocks should NOT be hoisted + callee: CREATE_BLOCK, + arguments: [ + `"div"`, + { content: `_hoisted_1` }, + [ + { + codegenNode: { content: `_hoisted_2` } + } + ] + ] + } + } + ] + }) + expect(generate(root).code).toMatchSnapshot() + }) + + test('should hoist v-for children if static', () => { + const { root, args } = transformWithHoist( + `
` + ) + expect(root.hoists).toMatchObject([ + createObjectMatcher({ + id: 'foo' + }), + { + callee: CREATE_VNODE, + arguments: [`"span"`] + } + ]) + const forBlockCodegen = args[2][0].codegenNode + expect(forBlockCodegen).toMatchObject({ + type: NodeTypes.JS_SEQUENCE_EXPRESSION, + expressions: [ + { callee: OPEN_BLOCK }, + { + callee: CREATE_BLOCK, + arguments: [ + FRAGMENT, + `null`, + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: RENDER_LIST + }, + genFlagText(PatchFlags.UNKEYED_FRAGMENT) + ] + } + ] + }) + const innerBlockCodegen = + forBlockCodegen.expressions[1].arguments[2].arguments[1].returns + expect(innerBlockCodegen).toMatchObject({ + type: NodeTypes.JS_SEQUENCE_EXPRESSION, + expressions: [ + { callee: OPEN_BLOCK }, + { + callee: CREATE_BLOCK, + arguments: [ + `"div"`, + { content: `_hoisted_1` }, + [ + { + codegenNode: { content: `_hoisted_2` } + } + ] + ] + } + ] + }) + expect(generate(root).code).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index d82f87ab242..2bd6e186c44 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -25,7 +25,7 @@ import { transformStyle } from '../../../compiler-dom/src/transforms/transformSt import { transformOn } from '../../src/transforms/vOn' import { transformBind } from '../../src/transforms/vBind' import { PatchFlags } from '@vue/shared' -import { createObjectMatcher } from '../testUtils' +import { createObjectMatcher, genFlagText } from '../testUtils' import { optimizeText } from '../../src/transforms/optimizeText' function parseWithElementTransform( @@ -324,7 +324,7 @@ describe('compiler: element transform', () => { `"div"`, `null`, `null`, - `${PatchFlags.NEED_PATCH} /* NEED_PATCH */` // should generate appropriate flag + genFlagText(PatchFlags.NEED_PATCH) // should generate appropriate flag ] }, { @@ -573,30 +573,30 @@ describe('compiler: element transform', () => { const { node: node2 } = parseWithBind(`
{{ foo }}
`) expect(node2.arguments.length).toBe(4) - expect(node2.arguments[3]).toBe(`${PatchFlags.TEXT} /* TEXT */`) + expect(node2.arguments[3]).toBe(genFlagText(PatchFlags.TEXT)) // multiple nodes, merged with optimize text const { node: node3 } = parseWithBind(`
foo {{ bar }} baz
`) expect(node3.arguments.length).toBe(4) - expect(node3.arguments[3]).toBe(`${PatchFlags.TEXT} /* TEXT */`) + expect(node3.arguments[3]).toBe(genFlagText(PatchFlags.TEXT)) }) test('CLASS', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe(`${PatchFlags.CLASS} /* CLASS */`) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.CLASS)) }) test('STYLE', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe(`${PatchFlags.STYLE} /* STYLE */`) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.STYLE)) }) test('PROPS', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(5) - expect(node.arguments[3]).toBe(`${PatchFlags.PROPS} /* PROPS */`) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.PROPS)) expect(node.arguments[4]).toBe(`["foo", "baz"]`) }) @@ -606,9 +606,7 @@ describe('compiler: element transform', () => { ) expect(node.arguments.length).toBe(5) expect(node.arguments[3]).toBe( - `${PatchFlags.PROPS | - PatchFlags.CLASS | - PatchFlags.STYLE} /* CLASS, STYLE, PROPS */` + genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS]) ) expect(node.arguments[4]).toBe(`["foo", "baz"]`) }) @@ -616,17 +614,13 @@ describe('compiler: element transform', () => { test('FULL_PROPS (v-bind)', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe( - `${PatchFlags.FULL_PROPS} /* FULL_PROPS */` - ) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS)) }) test('FULL_PROPS (dynamic key)', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe( - `${PatchFlags.FULL_PROPS} /* FULL_PROPS */` - ) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS)) }) test('FULL_PROPS (w/ others)', () => { @@ -634,34 +628,26 @@ describe('compiler: element transform', () => { `
` ) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe( - `${PatchFlags.FULL_PROPS} /* FULL_PROPS */` - ) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS)) }) test('NEED_PATCH (static ref)', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe( - `${PatchFlags.NEED_PATCH} /* NEED_PATCH */` - ) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH)) }) test('NEED_PATCH (dynamic ref)', () => { const { node } = parseWithBind(`
`) expect(node.arguments.length).toBe(4) - expect(node.arguments[3]).toBe( - `${PatchFlags.NEED_PATCH} /* NEED_PATCH */` - ) + expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH)) }) test('NEED_PATCH (custom directives)', () => { const { node } = parseWithBind(`
`) const vnodeCall = node.arguments[0] as CallExpression expect(vnodeCall.arguments.length).toBe(4) - expect(vnodeCall.arguments[3]).toBe( - `${PatchFlags.NEED_PATCH} /* NEED_PATCH */` - ) + expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH)) }) }) }) diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts index 2697358c056..ff6a4d4586f 100644 --- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts @@ -25,8 +25,7 @@ import { RENDER_SLOT } from '../../src/runtimeHelpers' import { PatchFlags } from '@vue/runtime-dom' -import { PatchFlagNames } from '@vue/shared' -import { createObjectMatcher } from '../testUtils' +import { createObjectMatcher, genFlagText } from '../testUtils' function parseWithForTransform( template: string, @@ -609,12 +608,8 @@ describe('compiler: v-for', () => { ] }, keyed - ? `${PatchFlags.KEYED_FRAGMENT} /* ${ - PatchFlagNames[PatchFlags.KEYED_FRAGMENT] - } */` - : `${PatchFlags.UNKEYED_FRAGMENT} /* ${ - PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT] - } */` + ? genFlagText(PatchFlags.KEYED_FRAGMENT) + : genFlagText(PatchFlags.UNKEYED_FRAGMENT) ] } ] @@ -842,9 +837,7 @@ describe('compiler: v-for', () => { } ] }, - `${PatchFlags.UNKEYED_FRAGMENT} /* ${ - PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT] - } */` + genFlagText(PatchFlags.UNKEYED_FRAGMENT) ] } } diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 1641ef8114c..bd1057f36df 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -18,8 +18,8 @@ import { trackVForSlotScopes } from '../../src/transforms/vSlot' import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers' -import { createObjectMatcher } from '../testUtils' -import { PatchFlags, PatchFlagNames } from '@vue/shared' +import { createObjectMatcher, genFlagText } from '../testUtils' +import { PatchFlags } from '@vue/shared' import { transformFor } from '../../src/transforms/vFor' import { transformIf } from '../../src/transforms/vIf' @@ -308,9 +308,7 @@ describe('compiler: transform component slots', () => { }), // nested slot should be forced dynamic, since scope variables // are not tracked as dependencies of the slot. - `${PatchFlags.DYNAMIC_SLOTS} /* ${ - PatchFlagNames[PatchFlags.DYNAMIC_SLOTS] - } */` + genFlagText(PatchFlags.DYNAMIC_SLOTS) ] } }, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 5fe82eae986..dcfc3732bbe 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -136,6 +136,10 @@ export interface SlotOutletNode extends BaseElementNode { export interface TemplateNode extends BaseElementNode { tagType: ElementTypes.TEMPLATE + codegenNode: + | ElementCodegenNode + | CodegenNodeWithDirective + | undefined } export interface TextNode extends Node { @@ -278,8 +282,7 @@ export interface ConditionalExpression extends Node { // createVNode(...) export interface ElementCodegenNode extends CallExpression { callee: typeof CREATE_VNODE - arguments: // tag, props, children, patchFlag, dynamicProps - + arguments: // tag, props, children, patchFlag, dynamicProps | [string | RuntimeHelper] | [string | RuntimeHelper, PropsExpression] | [string | RuntimeHelper, 'null' | PropsExpression, TemplateChildNode[]] @@ -305,8 +308,7 @@ export type ElementCodegenNodeWithDirective = CodegenNodeWithDirective< // createVNode(...) export interface ComponentCodegenNode extends CallExpression { callee: typeof CREATE_VNODE - arguments: // Comp, props, slots, patchFlag, dynamicProps - + arguments: // Comp, props, slots, patchFlag, dynamicProps | [string | RuntimeHelper] | [string | RuntimeHelper, PropsExpression] | [string | RuntimeHelper, 'null' | PropsExpression, SlotsExpression] @@ -394,8 +396,7 @@ export interface DirectiveArguments extends ArrayExpression { } export interface DirectiveArgumentNode extends ArrayExpression { - elements: // dir, exp, arg, modifiers - + elements: // dir, exp, arg, modifiers | [string] | [string, ExpressionNode] | [string, ExpressionNode, ExpressionNode] @@ -405,8 +406,7 @@ export interface DirectiveArgumentNode extends ArrayExpression { // renderSlot(...) export interface SlotOutletCodegenNode extends CallExpression { callee: typeof RENDER_SLOT - arguments: // $slots, name, props, fallback - + arguments: // $slots, name, props, fallback | [string, string | ExpressionNode] | [string, string | ExpressionNode, PropsExpression] | [ @@ -557,7 +557,7 @@ type InferCodegenNodeType = T extends typeof CREATE_VNODE : T extends typeof CREATE_BLOCK ? BlockElementCodegenNode | BlockComponentCodegenNode : T extends typeof APPLY_DIRECTIVES - ? CodegenNodeWithDirective + ? ElementCodegenNodeWithDirective | CompoenntCodegenNodeWithDirective : T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression export function createCallExpression( diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 363b7918e23..54977c35c7b 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -22,8 +22,8 @@ import { RuntimeHelper, helperNameMap } from './runtimeHelpers' -import { isVSlot, createBlockExpression, isSlotOutlet } from './utils' -import { hoistStatic } from './transforms/hoistStatic' +import { isVSlot, createBlockExpression } from './utils' +import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic' // There are two types of transforms: // @@ -229,26 +229,19 @@ export function transform(root: RootNode, options: TransformOptions) { function finalizeRoot(root: RootNode, context: TransformContext) { const { helper } = context const { children } = root - if (children.length === 1) { - const child = children[0] - if ( - child.type === NodeTypes.ELEMENT && - !isSlotOutlet(child) && - child.codegenNode && - child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION - ) { - // turn root element into a block - root.codegenNode = createBlockExpression( - child.codegenNode!.arguments, - context - ) - } else { - // - single , IfNode, ForNode: already blocks. - // - single text node: always patched. - // - transform calls without transformElement (only during tests) - // Just generate the node as-is - root.codegenNode = child - } + const child = children[0] + if (isSingleElementRoot(root, child) && child.codegenNode) { + // turn root element into a block + root.codegenNode = createBlockExpression( + child.codegenNode.arguments, + context + ) + } else if (children.length === 1) { + // - single , IfNode, ForNode: already blocks. + // - single text node: always patched. + // - transform calls without transformElement (only during tests) + // Just generate the node as-is + root.codegenNode = child } else if (children.length > 1) { // root has multiple nodes - return a fragment block. root.codegenNode = createBlockExpression( @@ -256,7 +249,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) { context ) } - // finalize meta information root.helpers = [...context.helpers] root.components = [...context.components] diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 155aa772596..b949db22ac4 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -5,20 +5,42 @@ import { ElementNode, ElementTypes, ElementCodegenNode, - ElementCodegenNodeWithDirective + ElementCodegenNodeWithDirective, + PlainElementNode, + ComponentNode, + TemplateNode } from '../ast' import { TransformContext } from '../transform' import { APPLY_DIRECTIVES } from '../runtimeHelpers' import { PatchFlags } from '@vue/shared' +import { isSlotOutlet } from '../utils' export function hoistStatic(root: RootNode, context: TransformContext) { - walk(root.children, context, new Map()) + walk( + root.children, + context, + new Map(), + 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( children: TemplateChildNode[], context: TransformContext, - resultCache: Map + resultCache: Map, + doNotHoistNode: boolean = false ) { for (let i = 0; i < children.length; i++) { const child = children[i] @@ -27,7 +49,7 @@ function walk( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT ) { - if (isStaticNode(child, resultCache)) { + if (!doNotHoistNode && isStaticNode(child, resultCache)) { // whole tree is static ;(child as any).codegenNode = context.hoist(child.codegenNode!) continue @@ -51,11 +73,16 @@ function walk( } } } - if (child.type === NodeTypes.ELEMENT || child.type === NodeTypes.FOR) { + if (child.type === NodeTypes.ELEMENT) { walk(child.children, context, resultCache) + } else if (child.type === NodeTypes.FOR) { + // Do not hoist v-for single child because it has to be a block + walk(child.children, context, resultCache, child.children.length === 1) } else if (child.type === NodeTypes.IF) { for (let i = 0; i < child.branches.length; i++) { - walk(child.branches[i].children, context, resultCache) + const branchChildren = child.branches[i].children + // Do not hoist v-if single child because it has to be a block + walk(branchChildren, context, resultCache, branchChildren.length === 1) } } }