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

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

View File

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

View File

@@ -12,14 +12,18 @@ import {
createCallExpression,
createFunctionExpression,
ElementTypes,
ObjectExpression,
createObjectExpression,
createObjectProperty,
TemplateChildNode,
CallExpression
createObjectProperty
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange, findProp, createBlockExpression } from '../utils'
import {
getInnerRange,
findProp,
createBlockExpression,
isTemplateNode,
isSlotOutlet,
injectProp
} from '../utils'
import {
RENDER_LIST,
OPEN_BLOCK,
@@ -28,6 +32,7 @@ import {
} from '../runtimeConstants'
import { processExpression } from './transformExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { PropsExpression } from './transformElement'
export const transformFor = createStructuralDirectiveTransform(
'for',
@@ -91,40 +96,52 @@ export const transformFor = createStructuralDirectiveTransform(
// finish the codegen now that all children have been traversed
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="...">
// 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(
[helper(FRAGMENT), childBlockProps, childBlockChildren],
[
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : `null`,
node.children
],
context
)
} else {
// Normal element v-for. Directly use the child's codegenNode arguments,
// but replace createVNode() with createBlock()
// Normal element v-for. Directly use the child's codegenNode
// arguments, but replace createVNode() with createBlock()
childBlock = createBlockExpression(
node.codegenNode!.arguments,
context

View File

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