feat(compiler): render <slot/> as block fragments

This commit is contained in:
Evan You 2019-10-03 12:03:14 -04:00
parent fc47029ed3
commit aa9245d55c
12 changed files with 268 additions and 191 deletions

View File

@ -274,16 +274,13 @@ describe('compiler: transform', () => {
test('single <slot/>', () => { test('single <slot/>', () => {
const ast = transformWithCodegen(`<slot/>`) const ast = transformWithCodegen(`<slot/>`)
expect(ast.codegenNode).toMatchObject( expect(ast.codegenNode).toMatchObject({
createBlockMatcher([ codegenNode: {
`_${FRAGMENT}`, type: NodeTypes.JS_CALL_EXPRESSION,
`null`, callee: `_${RENDER_SLOT}`,
{ arguments: ['$slots.default']
type: NodeTypes.JS_CALL_EXPRESSION, }
callee: `_${RENDER_SLOT}` })
}
])
)
}) })
test('single element', () => { test('single element', () => {

View File

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

View File

@ -37,10 +37,10 @@ exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
return function render() { return function render() {
with (this) { with (this) {
const { openBlock: _openBlock, renderSlot: _renderSlot, Fragment: _Fragment, createBlock: _createBlock, Empty: _Empty } = _Vue const { openBlock: _openBlock, renderSlot: _renderSlot, createBlock: _createBlock, Empty: _Empty } = _Vue
return (_openBlock(), ok return (_openBlock(), ok
? _createBlock(_Fragment, { key: 0 }, _renderSlot($slots.default)) ? _renderSlot($slots.default, { key: 0 })
: _createBlock(_Empty)) : _createBlock(_Empty))
} }
}" }"

View File

@ -567,7 +567,8 @@ describe('compiler: v-for', () => {
describe('codegen', () => { describe('codegen', () => {
function assertSharedCodegen( function assertSharedCodegen(
node: SequenceExpression, node: SequenceExpression,
keyed: boolean = false keyed: boolean = false,
customReturn: boolean = false
) { ) {
expect(node).toMatchObject({ expect(node).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@ -589,19 +590,21 @@ describe('compiler: v-for', () => {
{}, // to be asserted by each test {}, // to be asserted by each test
{ {
type: NodeTypes.JS_FUNCTION_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: { returns: customReturn
type: NodeTypes.JS_SEQUENCE_EXPRESSION, ? {}
expressions: [ : {
{ type: NodeTypes.JS_SEQUENCE_EXPRESSION,
type: NodeTypes.JS_CALL_EXPRESSION, expressions: [
callee: `_${OPEN_BLOCK}` {
}, type: NodeTypes.JS_CALL_EXPRESSION,
{ callee: `_${OPEN_BLOCK}`
type: NodeTypes.JS_CALL_EXPRESSION, },
callee: `_${CREATE_BLOCK}` {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${CREATE_BLOCK}`
}
]
} }
]
}
} }
] ]
}, },
@ -621,7 +624,10 @@ describe('compiler: v-for', () => {
return { return {
source: renderListArgs[0] as SimpleExpressionNode, source: renderListArgs[0] as SimpleExpressionNode,
params: (renderListArgs[1] as any).params, params: (renderListArgs[1] as any).params,
blockArgs: (renderListArgs[1] as any).returns.expressions[1].arguments returns: (renderListArgs[1] as any).returns,
blockArgs: customReturn
? null
: (renderListArgs[1] as any).returns.expressions[1].arguments
} }
} }
@ -715,17 +721,33 @@ describe('compiler: v-for', () => {
} = parseWithForTransform( } = parseWithForTransform(
'<template v-for="item in items"><slot/></template>' '<template v-for="item in items"><slot/></template>'
) )
expect(assertSharedCodegen(codegenNode)).toMatchObject({ expect(
assertSharedCodegen(codegenNode, false, true /* custom return */)
).toMatchObject({
source: { content: `items` }, source: { content: `items` },
params: [{ content: `item` }], params: [{ content: `item` }],
blockArgs: [ returns: {
`_${FRAGMENT}`, type: NodeTypes.JS_CALL_EXPRESSION,
`null`, callee: `_${RENDER_SLOT}`
{ }
type: NodeTypes.JS_CALL_EXPRESSION, })
callee: `_${RENDER_SLOT}` expect(generate(root).code).toMatchSnapshot()
} })
]
test('v-for on <slot/>', () => {
const {
root,
node: { codegenNode }
} = parseWithForTransform('<slot v-for="item in items"></slot>')
expect(
assertSharedCodegen(codegenNode, false, true /* custom return */)
).toMatchObject({
source: { content: `items` },
params: [{ content: `item` }],
returns: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`
}
}) })
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -794,7 +816,7 @@ describe('compiler: v-for', () => {
// should optimize v-if + v-for into a single Fragment block // should optimize v-if + v-for into a single Fragment block
arguments: [ arguments: [
`_${FRAGMENT}`, `_${FRAGMENT}`,
`{ key: 0 }`, createObjectMatcher({ key: `[0]` }),
{ {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_LIST}`, callee: `_${RENDER_LIST}`,

View File

@ -316,7 +316,10 @@ describe('compiler: v-if', () => {
assertSharedCodegen(codegenNode) assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`]) expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression .alternate as CallExpression
expect(branch2.arguments).toMatchObject([`_${EMPTY}`]) expect(branch2.arguments).toMatchObject([`_${EMPTY}`])
@ -333,7 +336,7 @@ describe('compiler: v-if', () => {
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([ expect(branch1.arguments).toMatchObject([
`_${FRAGMENT}`, `_${FRAGMENT}`,
`{ key: 0 }`, createObjectMatcher({ key: `[0]` }),
[ [
{ type: NodeTypes.ELEMENT, tag: 'div' }, { type: NodeTypes.ELEMENT, tag: 'div' },
{ type: NodeTypes.TEXT, content: `hello` }, { type: NodeTypes.TEXT, content: `hello` },
@ -351,17 +354,14 @@ describe('compiler: v-if', () => {
root, root,
node: { codegenNode } node: { codegenNode }
} = parseWithIfTransform(`<template v-if="ok"><slot/></template>`) } = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
assertSharedCodegen(codegenNode) // assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([ expect(branch1).toMatchObject({
`_${FRAGMENT}`, type: NodeTypes.JS_CALL_EXPRESSION,
`{ key: 0 }`, callee: `_${RENDER_SLOT}`,
{ arguments: ['$slots.default', createObjectMatcher({ key: `[0]` })]
type: NodeTypes.JS_CALL_EXPRESSION, })
callee: `_${RENDER_SLOT}`
}
])
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -373,10 +373,16 @@ describe('compiler: v-if', () => {
assertSharedCodegen(codegenNode) assertSharedCodegen(codegenNode)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`]) expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as CallExpression .alternate as CallExpression
expect(branch2.arguments).toMatchObject([`"p"`, `{ key: 1 }`]) expect(branch2.arguments).toMatchObject([
`"p"`,
createObjectMatcher({ key: `[1]` })
])
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -388,12 +394,15 @@ describe('compiler: v-if', () => {
assertSharedCodegen(codegenNode, 1) assertSharedCodegen(codegenNode, 1)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`]) expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as ConditionalExpression .alternate as ConditionalExpression
expect((branch2.consequent as CallExpression).arguments).toMatchObject([ expect((branch2.consequent as CallExpression).arguments).toMatchObject([
`"p"`, `"p"`,
`{ key: 1 }` createObjectMatcher({ key: `[1]` })
]) ])
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
@ -408,16 +417,19 @@ describe('compiler: v-if', () => {
assertSharedCodegen(codegenNode, 1) assertSharedCodegen(codegenNode, 1)
const branch1 = (codegenNode.expressions[1] as ConditionalExpression) const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
.consequent as CallExpression .consequent as CallExpression
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`]) expect(branch1.arguments).toMatchObject([
`"div"`,
createObjectMatcher({ key: `[0]` })
])
const branch2 = (codegenNode.expressions[1] as ConditionalExpression) const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
.alternate as ConditionalExpression .alternate as ConditionalExpression
expect((branch2.consequent as CallExpression).arguments).toMatchObject([ expect((branch2.consequent as CallExpression).arguments).toMatchObject([
`"p"`, `"p"`,
`{ key: 1 }` createObjectMatcher({ key: `[1]` })
]) ])
expect((branch2.alternate as CallExpression).arguments).toMatchObject([ expect((branch2.alternate as CallExpression).arguments).toMatchObject([
`_${FRAGMENT}`, `_${FRAGMENT}`,
`{ key: 2 }`, createObjectMatcher({ key: `[2]` }),
[ [
{ {
type: NodeTypes.TEXT, type: NodeTypes.TEXT,
@ -437,7 +449,7 @@ describe('compiler: v-if', () => {
expect(branch1.arguments[1]).toMatchObject({ expect(branch1.arguments[1]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${MERGE_PROPS}`, callee: `_${MERGE_PROPS}`,
arguments: [`{ key: 0 }`, { content: `obj` }] arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }]
}) })
}) })
@ -470,7 +482,7 @@ describe('compiler: v-if', () => {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${MERGE_PROPS}`, callee: `_${MERGE_PROPS}`,
arguments: [ arguments: [
`{ key: 0 }`, createObjectMatcher({ key: `[0]` }),
{ content: `obj` }, { content: `obj` },
createObjectMatcher({ createObjectMatcher({
id: 'foo' id: 'foo'
@ -487,9 +499,11 @@ describe('compiler: v-if', () => {
.consequent as CallExpression .consequent as CallExpression
expect(branch1.callee).toBe(`_${APPLY_DIRECTIVES}`) expect(branch1.callee).toBe(`_${APPLY_DIRECTIVES}`)
const realBranch = branch1.arguments[0] as CallExpression const realBranch = branch1.arguments[0] as CallExpression
expect(realBranch.arguments[1]).toBe(`{ key: 0 }`) expect(realBranch.arguments[1]).toMatchObject(
createObjectMatcher({ key: `[0]` })
)
}) })
test('with comments', () => {}) test.todo('with comments')
}) })
}) })

View File

@ -15,7 +15,7 @@ import {
import { isString, isArray } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants' import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
import { isVSlot, createBlockExpression } from './utils' import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
// There are two types of transforms: // There are two types of transforms:
// //
@ -192,22 +192,20 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
const { children } = root const { children } = root
if (children.length === 1) { if (children.length === 1) {
const child = children[0] const child = children[0]
if (child.type === NodeTypes.ELEMENT && child.codegenNode) { if (
// only child is a <slot/> - it needs to be in a fragment block. child.type === NodeTypes.ELEMENT &&
if (child.tagType === ElementTypes.SLOT) { !isSlotOutlet(child) &&
root.codegenNode = createBlockExpression( child.codegenNode
[helper(FRAGMENT), `null`, child.codegenNode!], ) {
context // turn root element into a block
) root.codegenNode = createBlockExpression(
} else { child.codegenNode!.arguments,
// turn root element into a block context
root.codegenNode = createBlockExpression( )
child.codegenNode!.arguments,
context
)
}
} else { } else {
// IfNode, ForNode, TextNodes or transform calls without transformElement. // - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// - 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
} }

View File

@ -101,15 +101,9 @@ export const transformElement: NodeTransform = (node, context) => {
if (hasDynamicTextChild) { if (hasDynamicTextChild) {
patchFlag |= PatchFlags.TEXT patchFlag |= PatchFlags.TEXT
} }
// pass directly if the only child is one of: // pass directly if the only child is a text node
// - text (plain / interpolation / expression) // (plain / interpolation / expression)
// - <slot> outlet (already an array) if (hasDynamicTextChild || type === NodeTypes.TEXT) {
if (
type === NodeTypes.TEXT ||
hasDynamicTextChild ||
(type === NodeTypes.ELEMENT &&
(child as ElementNode).tagType === ElementTypes.SLOT)
) {
args.push(child) args.push(child)
} else { } else {
args.push(node.children) args.push(node.children)
@ -171,7 +165,7 @@ export const transformElement: NodeTransform = (node, context) => {
} }
} }
type PropsExpression = ObjectExpression | CallExpression | ExpressionNode export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
export function buildProps( export function buildProps(
props: ElementNode['props'], props: ElementNode['props'],

View File

@ -1,19 +1,18 @@
import { NodeTransform } from '../transform' import { NodeTransform } from '../transform'
import { import {
NodeTypes, NodeTypes,
ElementTypes,
CompoundExpressionNode, CompoundExpressionNode,
createCompoundExpression, createCompoundExpression,
CallExpression, CallExpression,
createCallExpression createCallExpression
} from '../ast' } from '../ast'
import { isSimpleIdentifier } from '../utils' import { isSimpleIdentifier, isSlotOutlet } from '../utils'
import { buildProps } from './transformElement' import { buildProps } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeConstants' import { RENDER_SLOT } from '../runtimeConstants'
export const transformSlotOutlet: NodeTransform = (node, context) => { export const transformSlotOutlet: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT) { if (isSlotOutlet(node)) {
const { props, children, loc } = node const { props, children, loc } = node
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots` const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
let slot: string | CompoundExpressionNode = $slots + `.default` let slot: string | CompoundExpressionNode = $slots + `.default`

View File

@ -12,14 +12,18 @@ import {
createCallExpression, createCallExpression,
createFunctionExpression, createFunctionExpression,
ElementTypes, ElementTypes,
ObjectExpression,
createObjectExpression, createObjectExpression,
createObjectProperty, createObjectProperty
TemplateChildNode,
CallExpression
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange, findProp, createBlockExpression } from '../utils' import {
getInnerRange,
findProp,
createBlockExpression,
isTemplateNode,
isSlotOutlet,
injectProp
} from '../utils'
import { import {
RENDER_LIST, RENDER_LIST,
OPEN_BLOCK, OPEN_BLOCK,
@ -28,6 +32,7 @@ import {
} from '../runtimeConstants' } from '../runtimeConstants'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { PropsExpression } from './transformElement'
export const transformFor = createStructuralDirectiveTransform( export const transformFor = createStructuralDirectiveTransform(
'for', 'for',
@ -91,40 +96,52 @@ export const transformFor = createStructuralDirectiveTransform(
// finish the codegen now that all children have been traversed // finish the codegen now that all children have been traversed
let childBlock let childBlock
if (node.tagType === ElementTypes.TEMPLATE) { const isTemplate = isTemplateNode(node)
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? node.children[0]
: null
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode!
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
const existingProps = childBlock.arguments[1] as
| PropsExpression
| undefined
| 'null'
childBlock.arguments[1] = injectProp(
existingProps,
keyProperty,
context
)
}
} else if (isTemplate) {
// <template v-for="..."> // <template v-for="...">
// should genereate a fragment block for each loop // should genereate a fragment block for each loop
let childBlockProps: string | ObjectExpression = `null`
if (keyProp) {
childBlockProps = createObjectExpression([
createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
])
}
let childBlockChildren: TemplateChildNode[] | CallExpression =
node.children
// if the only child is a <slot/>, use it directly as fragment
// children since it already returns an array.
if (childBlockChildren.length === 1) {
const child = childBlockChildren[0]
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.SLOT
) {
childBlockChildren = child.codegenNode!
}
}
childBlock = createBlockExpression( childBlock = createBlockExpression(
[helper(FRAGMENT), childBlockProps, childBlockChildren], [
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children
],
context context
) )
} else { } else {
// Normal element v-for. Directly use the child's codegenNode arguments, // Normal element v-for. Directly use the child's codegenNode
// but replace createVNode() with createBlock() // arguments, but replace createVNode() with createBlock()
childBlock = createBlockExpression( childBlock = createBlockExpression(
node.codegenNode!.arguments, node.codegenNode!.arguments,
context context

View File

@ -16,11 +16,8 @@ import {
ConditionalExpression, ConditionalExpression,
CallExpression, CallExpression,
createSimpleExpression, createSimpleExpression,
JSChildNode,
ObjectExpression,
createObjectProperty, createObjectProperty,
Property, createObjectExpression
ExpressionNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
@ -30,9 +27,10 @@ import {
EMPTY, EMPTY,
FRAGMENT, FRAGMENT,
APPLY_DIRECTIVES, APPLY_DIRECTIVES,
MERGE_PROPS CREATE_VNODE
} from '../runtimeConstants' } from '../runtimeConstants'
import { isString } from '@vue/shared' import { injectProp } from '../utils'
import { PropsExpression } from './transformElement'
export const transformIf = createStructuralDirectiveTransform( export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
@ -153,81 +151,52 @@ function createCodegenNodeForBranch(
function createChildrenCodegenNode( function createChildrenCodegenNode(
branch: IfBranchNode, branch: IfBranchNode,
index: number, index: number,
{ helper }: TransformContext context: TransformContext
): CallExpression { ): CallExpression {
const keyExp = `{ key: ${index} }` const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(index + '', false)
)
const { children } = branch const { children } = branch
const child = children[0] const child = children[0]
const needFragmentWrapper = const needFragmentWrapper =
children.length !== 1 || children.length !== 1 || child.type !== NodeTypes.ELEMENT
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, createObjectExpression([keyProperty]),
children children
] ]
if (children.length === 1) { if (children.length === 1 && child.type === NodeTypes.FOR) {
// optimize away nested fragments when child is a ForNode // optimize away nested fragments when child is a ForNode
if (child.type === NodeTypes.FOR) { const forBlockArgs = (child.codegenNode.expressions[1] as CallExpression)
const forBlockArgs = (child.codegenNode .arguments
.expressions[1] as CallExpression).arguments // directly use the for block's children and patchFlag
// directly use the for block's children and patchFlag blockArgs[2] = forBlockArgs[2]
blockArgs[2] = forBlockArgs[2] blockArgs[3] = forBlockArgs[3]
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 {
const childCodegen = (child as ElementNode).codegenNode! const childCodegen = (child as ElementNode).codegenNode!
let vnodeCall = childCodegen let vnodeCall = childCodegen
// Element with custom directives. Locate the actual createVNode() call.
if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) { if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
vnodeCall = vnodeCall.arguments[0] as CallExpression vnodeCall = vnodeCall.arguments[0] as CallExpression
} }
// change child to a block // Change createVNode to createBlock.
vnodeCall.callee = helper(CREATE_BLOCK) // It's possible to have renderSlot() here as well - which already produces
// branch key // a block, so no need to change the callee. renderSlot() also accepts props
const existingProps = vnodeCall.arguments[1] // as the 2nd argument, so the key injection logic below works for it too.
if (!existingProps || existingProps === `null`) { if (vnodeCall.callee.includes(CREATE_VNODE)) {
vnodeCall.arguments[1] = keyExp vnodeCall.callee = helper(CREATE_BLOCK)
} else {
// inject branch key if not already have a key
const props = existingProps as
| CallExpression
| ObjectExpression
| ExpressionNode
if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
// merged props... add ours
// only inject key to object literal if it's the first argument so that
// if doesn't override user provided keys
const first = props.arguments[0] as string | JSChildNode
if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
first.properties.unshift(createKeyProperty(index))
} else {
props.arguments.unshift(keyExp)
}
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props.properties.unshift(createKeyProperty(index))
} else {
// single v-bind with expression
vnodeCall.arguments[1] = createCallExpression(helper(MERGE_PROPS), [
keyExp,
props
])
}
} }
// inject branch key
const existingProps = vnodeCall.arguments[1] as
| PropsExpression
| undefined
| 'null'
vnodeCall.arguments[1] = injectProp(existingProps, keyProperty, context)
return childCodegen return childCodegen
} }
} }
function createKeyProperty(index: number): Property {
return createObjectProperty(`key`, createSimpleExpression(index + '', false))
}

View File

@ -10,13 +10,18 @@ import {
DirectiveNode, DirectiveNode,
ElementTypes, ElementTypes,
TemplateChildNode, TemplateChildNode,
RootNode RootNode,
ObjectExpression,
Property,
JSChildNode,
createObjectExpression
} from './ast' } from './ast'
import { parse } from 'acorn' import { parse } from 'acorn'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants' import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeConstants'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
// cache node requires // cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph // lazy require dependencies so that they don't end up in rollup's dep graph
@ -165,5 +170,40 @@ export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
export const isTemplateNode = ( export const isTemplateNode = (
node: RootNode | TemplateChildNode node: RootNode | TemplateChildNode
): node is ElementNode => ): node is ElementNode & { tagType: ElementTypes.TEMPLATE } =>
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
export const isSlotOutlet = (
node: RootNode | TemplateChildNode
): node is ElementNode & { tagType: ElementTypes.SLOT } =>
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
export function injectProp(
props: PropsExpression | undefined | 'null',
prop: Property,
context: TransformContext
): ObjectExpression | CallExpression {
if (props == null || props === `null`) {
return createObjectExpression([prop])
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
// merged props... add ours
// only inject key to object literal if it's the first argument so that
// if doesn't override user provided keys
const first = props.arguments[0] as string | JSChildNode
if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
first.properties.unshift(prop)
} else {
props.arguments.unshift(createObjectExpression([prop]))
}
return props
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props.properties.unshift(prop)
return props
} else {
// single v-bind with expression, return a merged replacement
return createCallExpression(context.helper(MERGE_PROPS), [
createObjectExpression([prop]),
props
])
}
}

View File

@ -1,12 +1,25 @@
import { Slot } from '../componentSlots' import { Slot } from '../componentSlots'
import { VNodeChildren } from '../vnode' import {
VNodeChildren,
openBlock,
createBlock,
Fragment,
VNode
} from '../vnode'
export function renderSlot( export function renderSlot(
slot: Slot | undefined, slot: Slot | undefined,
props: any = {}, props: any = {},
// this is not a user-facing function, so the fallback is always generated by // this is not a user-facing function, so the fallback is always generated by
// the compiler. // the compiler and gurunteed to be an array
fallback?: string | VNodeChildren fallback?: VNodeChildren
): string | VNodeChildren | null { ): VNode {
return slot ? slot() : fallback || null return (
openBlock(),
createBlock(
Fragment,
{ key: props.key },
slot ? slot(props) : fallback || []
)
)
} }