fix(compiler): generate correct fragment children when it contains single text node or slot outlet

This commit is contained in:
Evan You 2019-10-01 23:53:52 -04:00
parent a477594d65
commit 3a95a2f148
10 changed files with 153 additions and 48 deletions

View File

@ -14,7 +14,9 @@ return function render() {
_toString(world.burn()), _toString(world.burn()),
(_openBlock(), ok (_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: _createBlock(_Fragment, { key: 1 }, \\"no\\")), : _createBlock(_Fragment, { key: 1 }, [
\\"no\\"
])),
_createVNode(_Fragment, null, _renderList(list, (value, index) => { _createVNode(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [ return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toString(value + index)) _createVNode(\\"span\\", null, _toString(value + index))
@ -37,7 +39,9 @@ return function render() {
toString(_ctx.world.burn()), toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok) (openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, \\"no\\")), : createBlock(Fragment, { key: 1 }, [
\\"no\\"
])),
createVNode(Fragment, null, renderList(_ctx.list, (value, index) => { createVNode(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [ return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, toString(value + index)) createVNode(\\"span\\", null, toString(value + index))
@ -59,7 +63,9 @@ export default function render() {
_toString(_ctx.world.burn()), _toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok) (openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, \\"no\\")), : createBlock(Fragment, { key: 1 }, [
\\"no\\"
])),
createVNode(Fragment, null, renderList(_ctx.list, (value, index) => { createVNode(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [ return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, _toString(value + index)) createVNode(\\"span\\", null, _toString(value + index))

View File

@ -104,6 +104,20 @@ return function render() {
}" }"
`; `;
exports[`compiler: v-for codegen template v-for w/ <slot/> 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { renderList: _renderList, createVNode: _createVNode, Fragment: _Fragment, renderSlot: _renderSlot, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _createVNode(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createBlock(_Fragment, null, _renderSlot($slots.default)))
}), 128 /* UNKEYED_FRAGMENT */)
}
}"
`;
exports[`compiler: v-for codegen v-if + v-for 1`] = ` exports[`compiler: v-for codegen v-if + v-for 1`] = `
"const _Vue = Vue "const _Vue = Vue

View File

@ -32,6 +32,20 @@ return function render() {
}" }"
`; `;
exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { openBlock: _openBlock, renderSlot: _renderSlot, Fragment: _Fragment, createBlock: _createBlock, Empty: _Empty } = _Vue
return (_openBlock(), ok
? _createBlock(_Fragment, { key: 0 }, _renderSlot($slots.default))
: _createBlock(_Empty))
}
}"
`;
exports[`compiler: v-if codegen v-if + v-else 1`] = ` exports[`compiler: v-if codegen v-if + v-else 1`] = `
"const _Vue = Vue "const _Vue = Vue
@ -57,7 +71,9 @@ return function render() {
? _createBlock(\\"div\\", { key: 0 }) ? _createBlock(\\"div\\", { key: 0 })
: orNot : orNot
? _createBlock(\\"p\\", { key: 1 }) ? _createBlock(\\"p\\", { key: 1 })
: _createBlock(_Fragment, { key: 2 }, \\"fine\\")) : _createBlock(_Fragment, { key: 2 }, [
\\"fine\\"
]))
} }
}" }"
`; `;

View File

@ -4,6 +4,7 @@ import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformSlotOutlet } from '../../src/transforms/transfromSlotOutlet'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
import { import {
ForNode, ForNode,
@ -20,7 +21,8 @@ import {
CREATE_BLOCK, CREATE_BLOCK,
FRAGMENT, FRAGMENT,
RENDER_LIST, RENDER_LIST,
CREATE_VNODE CREATE_VNODE,
RENDER_SLOT
} from '../../src/runtimeConstants' } from '../../src/runtimeConstants'
import { PatchFlags } from '@vue/runtime-dom' import { PatchFlags } from '@vue/runtime-dom'
import { PatchFlagNames } from '@vue/shared' import { PatchFlagNames } from '@vue/shared'
@ -36,6 +38,7 @@ function parseWithForTransform(
transformIf, transformIf,
transformFor, transformFor,
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers ? [transformExpression] : []),
transformSlotOutlet,
transformElement transformElement
], ],
directiveTransforms: { directiveTransforms: {
@ -692,6 +695,28 @@ describe('compiler: v-for', () => {
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('template v-for w/ <slot/>', () => {
const {
root,
node: { codegenNode }
} = parseWithForTransform(
'<template v-for="item in items"><slot/></template>'
)
expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
blockArgs: [
`_${FRAGMENT}`,
`null`,
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`
}
]
})
expect(generate(root).code).toMatchSnapshot()
})
test('keyed v-for', () => { test('keyed v-for', () => {
const { const {
root, root,

View File

@ -2,6 +2,7 @@ import { parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformSlotOutlet } from '../../src/transforms/transfromSlotOutlet'
import { import {
IfNode, IfNode,
NodeTypes, NodeTypes,
@ -21,7 +22,8 @@ import {
EMPTY, EMPTY,
FRAGMENT, FRAGMENT,
MERGE_PROPS, MERGE_PROPS,
APPLY_DIRECTIVES APPLY_DIRECTIVES,
RENDER_SLOT
} from '../../src/runtimeConstants' } from '../../src/runtimeConstants'
import { createObjectMatcher } from '../testUtils' import { createObjectMatcher } from '../testUtils'
@ -32,7 +34,7 @@ function parseWithIfTransform(
) { ) {
const ast = parse(template, options) const ast = parse(template, options)
transform(ast, { transform(ast, {
nodeTransforms: [transformIf, transformElement], nodeTransforms: [transformIf, transformSlotOutlet, transformElement],
...options ...options
}) })
if (!options.onError) { if (!options.onError) {
@ -344,6 +346,25 @@ describe('compiler: v-if', () => {
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('template v-if w/ single <slot/> child', () => {
const {
root,
node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression
expect(branch1.arguments).toMatchObject([
`_${FRAGMENT}`,
`{ key: 0 }`,
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`
}
])
expect(generate(root).code).toMatchSnapshot()
})
test('v-if + v-else', () => { test('v-if + v-else', () => {
const { const {
root, root,

View File

@ -176,7 +176,7 @@ export type JSChildNode =
export interface CallExpression extends Node { export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION type: NodeTypes.JS_CALL_EXPRESSION
callee: string callee: string
arguments: (string | JSChildNode | TemplateChildNode[])[] arguments: (string | JSChildNode | TemplateChildNode | TemplateChildNode[])[]
} }
export interface ObjectExpression extends Node { export interface ObjectExpression extends Node {

View File

@ -1,7 +1,6 @@
import { import {
RootNode, RootNode,
TemplateChildNode, TemplateChildNode,
ElementNode,
TextNode, TextNode,
CommentNode, CommentNode,
ExpressionNode, ExpressionNode,
@ -15,7 +14,6 @@ import {
InterpolationNode, InterpolationNode,
CompoundExpressionNode, CompoundExpressionNode,
SimpleExpressionNode, SimpleExpressionNode,
ElementTypes,
FunctionExpression, FunctionExpression,
SequenceExpression, SequenceExpression,
ConditionalExpression ConditionalExpression
@ -232,7 +230,8 @@ export function generate(
// generate the VNode tree expression // generate the VNode tree expression
push(`return `) push(`return `)
genChildren(ast.children, context, true)
genRoot(ast, context)
if (useWithBlock) { if (useWithBlock) {
deindent() deindent()
@ -257,31 +256,13 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
context.newline() context.newline()
} }
// This will generate a single vnode call if: function genRoot(root: RootNode, context: CodegenContext) {
// - The target position explicitly allows a single node (root, if, for) // TODO handle blocks
// - The list has length === 1, AND The only child is a: const { children } = root
// - text if (children.length === 0) {
// - <slot> outlet, which always produces an array context.push(`null`)
function genChildren( } else if (children.length === 1) {
children: TemplateChildNode[], genNode(children[0], context)
context: CodegenContext,
allowSingle: boolean = false
) {
if (!children.length) {
return context.push(`null`)
}
const child = children[0]
const type = child.type
if (
children.length === 1 &&
(allowSingle ||
type === NodeTypes.TEXT ||
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION ||
(type === NodeTypes.ELEMENT &&
(child as ElementNode).tagType === ElementTypes.SLOT))
) {
genNode(child, context)
} else { } else {
genNodeListAsArray(children, context) genNodeListAsArray(children, context)
} }
@ -316,7 +297,7 @@ function genNodeList(
if (isString(node)) { if (isString(node)) {
push(node) push(node)
} else if (isArray(node)) { } else if (isArray(node)) {
genChildren(node, context) genNodeListAsArray(node, context)
} else { } else {
genNode(node, context) genNode(node, context)
} }

View File

@ -87,8 +87,26 @@ export const transformElement: NodeTransform = (node, context) => {
patchFlag |= PatchFlags.DYNAMIC_SLOTS patchFlag |= PatchFlags.DYNAMIC_SLOTS
} }
} else { } else {
// only v-for fragments will have keyed/unkeyed flags if (node.children.length === 1) {
args.push(node.children) const child = node.children[0]
const type = child.type
// pass directly if the only child is one of:
// - text (plain / interpolation / expression)
// - <slot> outlet (already an array)
if (
type === NodeTypes.TEXT ||
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION ||
(type === NodeTypes.ELEMENT &&
(child as ElementNode).tagType === ElementTypes.SLOT)
) {
args.push(child)
} else {
args.push(node.children)
}
} else {
args.push(node.children)
}
} }
} }
// patchFlag & dynamicPropNames // patchFlag & dynamicPropNames

View File

@ -14,7 +14,9 @@ import {
ElementTypes, ElementTypes,
ObjectExpression, ObjectExpression,
createObjectExpression, createObjectExpression,
createObjectProperty createObjectProperty,
TemplateChildNode,
CallExpression
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange, findProp } from '../utils' import { getInnerRange, findProp } from '../utils'
@ -120,12 +122,23 @@ export const transformFor = createStructuralDirectiveTransform(
) )
]) ])
} }
let childBlockChildren: TemplateChildNode[] | CallExpression =
node.children
if (childBlockChildren.length === 1) {
const child = childBlockChildren[0]
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.SLOT
) {
childBlockChildren = child.codegenNode!
}
}
childBlock = createSequenceExpression([ childBlock = createSequenceExpression([
createCallExpression(helper(OPEN_BLOCK)), createCallExpression(helper(OPEN_BLOCK)),
createCallExpression(helper(CREATE_BLOCK), [ createCallExpression(helper(CREATE_BLOCK), [
helper(FRAGMENT), helper(FRAGMENT),
childBlockProps, childBlockProps,
node.children childBlockChildren
]) ])
]) ])
} else { } else {

View File

@ -164,19 +164,30 @@ function createChildrenCodegenNode(
const { children } = branch const { children } = branch
const child = children[0] const child = children[0]
const needFragmentWrapper = const needFragmentWrapper =
children.length > 1 || child.type !== NodeTypes.ELEMENT children.length !== 1 ||
child.type !== NodeTypes.ELEMENT ||
child.tagType === ElementTypes.SLOT
if (needFragmentWrapper) { if (needFragmentWrapper) {
const blockArgs: CallExpression['arguments'] = [ const blockArgs: CallExpression['arguments'] = [
helper(FRAGMENT), helper(FRAGMENT),
keyExp, keyExp,
children children
] ]
// optimize away nested fragments when child is a ForNode if (children.length === 1) {
if (children.length === 1 && child.type === NodeTypes.FOR) { // optimize away nested fragments when child is a ForNode
const forBlockExp = child.codegenNode if (child.type === NodeTypes.FOR) {
// directly use the for block's children and patchFlag const forBlockArgs = child.codegenNode.arguments
blockArgs[2] = forBlockExp.arguments[2] // directly use the for block's children and patchFlag
blockArgs[3] = forBlockExp.arguments[3] blockArgs[2] = forBlockArgs[2]
blockArgs[3] = forBlockArgs[3]
} else if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.SLOT
) {
// <template v-if="..."><slot/></template>
// since slot always returns array, use it directly as the fragment children.
blockArgs[2] = child.codegenNode!
}
} }
return createCallExpression(helper(CREATE_BLOCK), blockArgs) return createCallExpression(helper(CREATE_BLOCK), blockArgs)
} else { } else {