test: tests for hoistStatic

This commit is contained in:
Evan You 2019-10-07 17:12:22 -04:00
parent 802ecccc49
commit 57a5c61320
9 changed files with 655 additions and 83 deletions

View File

@ -7,7 +7,7 @@ import {
ElementCodegenNode ElementCodegenNode
} from '../src' } from '../src'
import { CREATE_VNODE } from '../src/runtimeHelpers' import { CREATE_VNODE } from '../src/runtimeHelpers'
import { isString } from '@vue/shared' import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared'
const leadingBracketRE = /^\[/ const leadingBracketRE = /^\[/
const bracketsRE = /^\[|\]$/g 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]} */`
}
}

View File

@ -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))
]))
}
}"
`;

View File

@ -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(`<div/>`)
expect(root.hoists.length).toBe(0)
expect(args).toEqual([`"div"`])
expect(generate(root).code).toMatchSnapshot()
})
test('hoist simple element', () => {
const { root, args } = transformWithHoist(
`<div><span class="inline">hello</span></div>`
)
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(
`<div><p><span/><span/></p></div>`
)
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(`<div><span/><div/></div>`)
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(`<div><Comp/></div>`)
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(`<div><div :id="foo"/></div>`)
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(
`<div><div id="foo" v-foo/></div>`
)
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(
`<div><div id="foo">{{ hello }}</div></div>`
)
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(
`<div><div id="foo"><Comp/></div></div>`
)
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(
`<div><div v-if="ok" id="foo"><span/></div></div>`
)
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(
`<div><div v-for="i in list" id="foo"><span/></div></div>`
)
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()
})
})

View File

@ -25,7 +25,7 @@ import { transformStyle } from '../../../compiler-dom/src/transforms/transformSt
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { createObjectMatcher } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
import { optimizeText } from '../../src/transforms/optimizeText' import { optimizeText } from '../../src/transforms/optimizeText'
function parseWithElementTransform( function parseWithElementTransform(
@ -324,7 +324,7 @@ describe('compiler: element transform', () => {
`"div"`, `"div"`,
`null`, `null`,
`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(`<div>{{ foo }}</div>`) const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`)
expect(node2.arguments.length).toBe(4) 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 // multiple nodes, merged with optimize text
const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`) const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`)
expect(node3.arguments.length).toBe(4) expect(node3.arguments.length).toBe(4)
expect(node3.arguments[3]).toBe(`${PatchFlags.TEXT} /* TEXT */`) expect(node3.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
}) })
test('CLASS', () => { test('CLASS', () => {
const { node } = parseWithBind(`<div :class="foo" />`) const { node } = parseWithBind(`<div :class="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(`${PatchFlags.CLASS} /* CLASS */`) expect(node.arguments[3]).toBe(genFlagText(PatchFlags.CLASS))
}) })
test('STYLE', () => { test('STYLE', () => {
const { node } = parseWithBind(`<div :style="foo" />`) const { node } = parseWithBind(`<div :style="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe(`${PatchFlags.STYLE} /* STYLE */`) expect(node.arguments[3]).toBe(genFlagText(PatchFlags.STYLE))
}) })
test('PROPS', () => { test('PROPS', () => {
const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`) const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
expect(node.arguments.length).toBe(5) 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"]`) expect(node.arguments[4]).toBe(`["foo", "baz"]`)
}) })
@ -606,9 +606,7 @@ describe('compiler: element transform', () => {
) )
expect(node.arguments.length).toBe(5) expect(node.arguments.length).toBe(5)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(
`${PatchFlags.PROPS | genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS])
PatchFlags.CLASS |
PatchFlags.STYLE} /* CLASS, STYLE, PROPS */`
) )
expect(node.arguments[4]).toBe(`["foo", "baz"]`) expect(node.arguments[4]).toBe(`["foo", "baz"]`)
}) })
@ -616,17 +614,13 @@ describe('compiler: element transform', () => {
test('FULL_PROPS (v-bind)', () => { test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`) const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
`${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
)
}) })
test('FULL_PROPS (dynamic key)', () => { test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(`<div :[foo]="bar" />`) const { node } = parseWithBind(`<div :[foo]="bar" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
`${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
)
}) })
test('FULL_PROPS (w/ others)', () => { test('FULL_PROPS (w/ others)', () => {
@ -634,34 +628,26 @@ describe('compiler: element transform', () => {
`<div id="foo" v-bind="bar" :class="cls" />` `<div id="foo" v-bind="bar" :class="cls" />`
) )
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
`${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
)
}) })
test('NEED_PATCH (static ref)', () => { test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(`<div ref="foo" />`) const { node } = parseWithBind(`<div ref="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
`${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
)
}) })
test('NEED_PATCH (dynamic ref)', () => { test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(`<div :ref="foo" />`) const { node } = parseWithBind(`<div :ref="foo" />`)
expect(node.arguments.length).toBe(4) expect(node.arguments.length).toBe(4)
expect(node.arguments[3]).toBe( expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
`${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
)
}) })
test('NEED_PATCH (custom directives)', () => { test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(`<div v-foo />`) const { node } = parseWithBind(`<div v-foo />`)
const vnodeCall = node.arguments[0] as CallExpression const vnodeCall = node.arguments[0] as CallExpression
expect(vnodeCall.arguments.length).toBe(4) expect(vnodeCall.arguments.length).toBe(4)
expect(vnodeCall.arguments[3]).toBe( expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
`${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
)
}) })
}) })
}) })

View File

@ -25,8 +25,7 @@ import {
RENDER_SLOT RENDER_SLOT
} from '../../src/runtimeHelpers' } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/runtime-dom' import { PatchFlags } from '@vue/runtime-dom'
import { PatchFlagNames } from '@vue/shared' import { createObjectMatcher, genFlagText } from '../testUtils'
import { createObjectMatcher } from '../testUtils'
function parseWithForTransform( function parseWithForTransform(
template: string, template: string,
@ -609,12 +608,8 @@ describe('compiler: v-for', () => {
] ]
}, },
keyed keyed
? `${PatchFlags.KEYED_FRAGMENT} /* ${ ? genFlagText(PatchFlags.KEYED_FRAGMENT)
PatchFlagNames[PatchFlags.KEYED_FRAGMENT] : genFlagText(PatchFlags.UNKEYED_FRAGMENT)
} */`
: `${PatchFlags.UNKEYED_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT]
} */`
] ]
} }
] ]
@ -842,9 +837,7 @@ describe('compiler: v-for', () => {
} }
] ]
}, },
`${PatchFlags.UNKEYED_FRAGMENT} /* ${ genFlagText(PatchFlags.UNKEYED_FRAGMENT)
PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT]
} */`
] ]
} }
} }

View File

@ -18,8 +18,8 @@ import {
trackVForSlotScopes trackVForSlotScopes
} from '../../src/transforms/vSlot' } from '../../src/transforms/vSlot'
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers' import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers'
import { createObjectMatcher } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
@ -308,9 +308,7 @@ describe('compiler: transform component slots', () => {
}), }),
// nested slot should be forced dynamic, since scope variables // nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot. // are not tracked as dependencies of the slot.
`${PatchFlags.DYNAMIC_SLOTS} /* ${ genFlagText(PatchFlags.DYNAMIC_SLOTS)
PatchFlagNames[PatchFlags.DYNAMIC_SLOTS]
} */`
] ]
} }
}, },

View File

@ -136,6 +136,10 @@ export interface SlotOutletNode extends BaseElementNode {
export interface TemplateNode extends BaseElementNode { export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE tagType: ElementTypes.TEMPLATE
codegenNode:
| ElementCodegenNode
| CodegenNodeWithDirective<ElementCodegenNode>
| undefined
} }
export interface TextNode extends Node { export interface TextNode extends Node {
@ -279,7 +283,6 @@ export interface ConditionalExpression extends Node {
export interface ElementCodegenNode extends CallExpression { export interface ElementCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE callee: typeof CREATE_VNODE
arguments: // tag, props, children, patchFlag, dynamicProps arguments: // tag, props, children, patchFlag, dynamicProps
| [string | RuntimeHelper] | [string | RuntimeHelper]
| [string | RuntimeHelper, PropsExpression] | [string | RuntimeHelper, PropsExpression]
| [string | RuntimeHelper, 'null' | PropsExpression, TemplateChildNode[]] | [string | RuntimeHelper, 'null' | PropsExpression, TemplateChildNode[]]
@ -306,7 +309,6 @@ export type ElementCodegenNodeWithDirective = CodegenNodeWithDirective<
export interface ComponentCodegenNode extends CallExpression { export interface ComponentCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE callee: typeof CREATE_VNODE
arguments: // Comp, props, slots, patchFlag, dynamicProps arguments: // Comp, props, slots, patchFlag, dynamicProps
| [string | RuntimeHelper] | [string | RuntimeHelper]
| [string | RuntimeHelper, PropsExpression] | [string | RuntimeHelper, PropsExpression]
| [string | RuntimeHelper, 'null' | PropsExpression, SlotsExpression] | [string | RuntimeHelper, 'null' | PropsExpression, SlotsExpression]
@ -395,7 +397,6 @@ export interface DirectiveArguments extends ArrayExpression {
export interface DirectiveArgumentNode extends ArrayExpression { export interface DirectiveArgumentNode extends ArrayExpression {
elements: // dir, exp, arg, modifiers elements: // dir, exp, arg, modifiers
| [string] | [string]
| [string, ExpressionNode] | [string, ExpressionNode]
| [string, ExpressionNode, ExpressionNode] | [string, ExpressionNode, ExpressionNode]
@ -406,7 +407,6 @@ export interface DirectiveArgumentNode extends ArrayExpression {
export interface SlotOutletCodegenNode extends CallExpression { export interface SlotOutletCodegenNode extends CallExpression {
callee: typeof RENDER_SLOT callee: typeof RENDER_SLOT
arguments: // $slots, name, props, fallback arguments: // $slots, name, props, fallback
| [string, string | ExpressionNode] | [string, string | ExpressionNode]
| [string, string | ExpressionNode, PropsExpression] | [string, string | ExpressionNode, PropsExpression]
| [ | [
@ -557,7 +557,7 @@ type InferCodegenNodeType<T> = T extends typeof CREATE_VNODE
: T extends typeof CREATE_BLOCK : T extends typeof CREATE_BLOCK
? BlockElementCodegenNode | BlockComponentCodegenNode ? BlockElementCodegenNode | BlockComponentCodegenNode
: T extends typeof APPLY_DIRECTIVES : T extends typeof APPLY_DIRECTIVES
? CodegenNodeWithDirective<ElementCodegenNode | ComponentCodegenNode> ? ElementCodegenNodeWithDirective | CompoenntCodegenNodeWithDirective
: T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression : T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression
export function createCallExpression<T extends CallExpression['callee']>( export function createCallExpression<T extends CallExpression['callee']>(

View File

@ -22,8 +22,8 @@ import {
RuntimeHelper, RuntimeHelper,
helperNameMap helperNameMap
} from './runtimeHelpers' } from './runtimeHelpers'
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils' import { isVSlot, createBlockExpression } from './utils'
import { hoistStatic } from './transforms/hoistStatic' import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
// There are two types of transforms: // There are two types of transforms:
// //
@ -229,26 +229,19 @@ export function transform(root: RootNode, options: TransformOptions) {
function finalizeRoot(root: RootNode, context: TransformContext) { function finalizeRoot(root: RootNode, context: TransformContext) {
const { helper } = context const { helper } = context
const { children } = root const { children } = root
if (children.length === 1) {
const child = children[0] const child = children[0]
if ( if (isSingleElementRoot(root, child) && child.codegenNode) {
child.type === NodeTypes.ELEMENT &&
!isSlotOutlet(child) &&
child.codegenNode &&
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION
) {
// turn root element into a block // turn root element into a block
root.codegenNode = createBlockExpression( root.codegenNode = createBlockExpression(
child.codegenNode!.arguments, child.codegenNode.arguments,
context context
) )
} else { } else if (children.length === 1) {
// - single <slot/>, IfNode, ForNode: already blocks. // - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched. // - single text node: always patched.
// - transform calls without transformElement (only during tests) // - transform calls without transformElement (only during tests)
// Just generate the node as-is // Just generate the node as-is
root.codegenNode = child root.codegenNode = child
}
} else if (children.length > 1) { } else if (children.length > 1) {
// root has multiple nodes - return a fragment block. // root has multiple nodes - return a fragment block.
root.codegenNode = createBlockExpression( root.codegenNode = createBlockExpression(
@ -256,7 +249,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
context context
) )
} }
// finalize meta information // finalize meta information
root.helpers = [...context.helpers] root.helpers = [...context.helpers]
root.components = [...context.components] root.components = [...context.components]

View File

@ -5,20 +5,42 @@ import {
ElementNode, ElementNode,
ElementTypes, ElementTypes,
ElementCodegenNode, ElementCodegenNode,
ElementCodegenNodeWithDirective ElementCodegenNodeWithDirective,
PlainElementNode,
ComponentNode,
TemplateNode
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
import { APPLY_DIRECTIVES } from '../runtimeHelpers' import { APPLY_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { isSlotOutlet } from '../utils'
export function hoistStatic(root: RootNode, context: TransformContext) { export function hoistStatic(root: RootNode, context: TransformContext) {
walk(root.children, context, new Map<TemplateChildNode, boolean>()) 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( function walk(
children: TemplateChildNode[], children: TemplateChildNode[],
context: TransformContext, context: TransformContext,
resultCache: Map<TemplateChildNode, boolean> resultCache: Map<TemplateChildNode, boolean>,
doNotHoistNode: boolean = false
) { ) {
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
@ -27,7 +49,7 @@ function walk(
child.type === NodeTypes.ELEMENT && child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT child.tagType === ElementTypes.ELEMENT
) { ) {
if (isStaticNode(child, resultCache)) { if (!doNotHoistNode && isStaticNode(child, resultCache)) {
// whole tree is static // whole tree is static
;(child as any).codegenNode = context.hoist(child.codegenNode!) ;(child as any).codegenNode = context.hoist(child.codegenNode!)
continue 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) 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) { } else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) { 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)
} }
} }
} }