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(
+ ``
+ )
+ 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)
}
}
}