fix(slots): ensure different branches of dynamic slots have different keys

fix #6202
This commit is contained in:
Evan You 2022-08-30 15:55:09 +08:00
parent 96eb745254
commit 00036bb52c
8 changed files with 69 additions and 20 deletions

View File

@ -43,7 +43,8 @@ export function render(_ctx, _cache) {
name: \\"foo\\", name: \\"foo\\",
fn: _withCtx(() => [ fn: _withCtx(() => [
_createElementVNode(\\"div\\") _createElementVNode(\\"div\\")
]) ]),
key: \\"0\\"
} }
: undefined, : undefined,
_renderList(_ctx.list, (i) => { _renderList(_ctx.list, (i) => {

View File

@ -56,7 +56,8 @@ return function render(_ctx, _cache) {
(_ctx.ok) (_ctx.ok)
? { ? {
name: \\"one\\", name: \\"one\\",
fn: _withCtx((props) => [_toDisplayString(props)]) fn: _withCtx((props) => [_toDisplayString(props)]),
key: \\"0\\"
} }
: undefined : undefined
]), 1024 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
@ -76,16 +77,19 @@ return function render(_ctx, _cache) {
ok ok
? { ? {
name: \\"one\\", name: \\"one\\",
fn: _withCtx(() => [\\"foo\\"]) fn: _withCtx(() => [\\"foo\\"]),
key: \\"0\\"
} }
: orNot : orNot
? { ? {
name: \\"two\\", name: \\"two\\",
fn: _withCtx((props) => [\\"bar\\"]) fn: _withCtx((props) => [\\"bar\\"]),
key: \\"1\\"
} }
: { : {
name: \\"one\\", name: \\"one\\",
fn: _withCtx(() => [\\"baz\\"]) fn: _withCtx(() => [\\"baz\\"]),
key: \\"2\\"
} }
]), 1024 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))
} }
@ -105,7 +109,8 @@ return function render(_ctx, _cache) {
ok ok
? { ? {
name: \\"one\\", name: \\"one\\",
fn: _withCtx(() => [\\"hello\\"]) fn: _withCtx(() => [\\"hello\\"]),
key: \\"0\\"
} }
: undefined : undefined
]), 1024 /* DYNAMIC_SLOTS */)) ]), 1024 /* DYNAMIC_SLOTS */))

View File

@ -568,7 +568,8 @@ describe('compiler: transform component slots', () => {
fn: { fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [{ type: NodeTypes.TEXT, content: `hello` }] returns: [{ type: NodeTypes.TEXT, content: `hello` }]
} },
key: `0`
}), }),
alternate: { alternate: {
content: `undefined`, content: `undefined`,
@ -616,7 +617,8 @@ describe('compiler: transform component slots', () => {
content: { content: `props` } content: { content: `props` }
} }
] ]
} },
key: `0`
}), }),
alternate: { alternate: {
content: `undefined`, content: `undefined`,
@ -660,7 +662,8 @@ describe('compiler: transform component slots', () => {
type: NodeTypes.JS_FUNCTION_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined, params: undefined,
returns: [{ type: NodeTypes.TEXT, content: `foo` }] returns: [{ type: NodeTypes.TEXT, content: `foo` }]
} },
key: `0`
}), }),
alternate: { alternate: {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
@ -671,7 +674,8 @@ describe('compiler: transform component slots', () => {
type: NodeTypes.JS_FUNCTION_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: { content: `props` }, params: { content: `props` },
returns: [{ type: NodeTypes.TEXT, content: `bar` }] returns: [{ type: NodeTypes.TEXT, content: `bar` }]
} },
key: `1`
}), }),
alternate: createObjectMatcher({ alternate: createObjectMatcher({
name: `one`, name: `one`,
@ -679,7 +683,8 @@ describe('compiler: transform component slots', () => {
type: NodeTypes.JS_FUNCTION_EXPRESSION, type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined, params: undefined,
returns: [{ type: NodeTypes.TEXT, content: `baz` }] returns: [{ type: NodeTypes.TEXT, content: `baz` }]
} },
key: `2`
}) })
} }
} }

View File

@ -160,6 +160,7 @@ export function buildSlots(
let hasNamedDefaultSlot = false let hasNamedDefaultSlot = false
const implicitDefaultChildren: TemplateChildNode[] = [] const implicitDefaultChildren: TemplateChildNode[] = []
const seenSlotNames = new Set<string>() const seenSlotNames = new Set<string>()
let conditionalBranchIndex = 0
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const slotElement = children[i] const slotElement = children[i]
@ -210,7 +211,7 @@ export function buildSlots(
dynamicSlots.push( dynamicSlots.push(
createConditionalExpression( createConditionalExpression(
vIf.exp!, vIf.exp!,
buildDynamicSlot(slotName, slotFunction), buildDynamicSlot(slotName, slotFunction, conditionalBranchIndex++),
defaultFallback defaultFallback
) )
) )
@ -243,10 +244,14 @@ export function buildSlots(
conditional.alternate = vElse.exp conditional.alternate = vElse.exp
? createConditionalExpression( ? createConditionalExpression(
vElse.exp, vElse.exp,
buildDynamicSlot(slotName, slotFunction), buildDynamicSlot(
slotName,
slotFunction,
conditionalBranchIndex++
),
defaultFallback defaultFallback
) )
: buildDynamicSlot(slotName, slotFunction) : buildDynamicSlot(slotName, slotFunction, conditionalBranchIndex++)
} else { } else {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc) createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
@ -369,12 +374,19 @@ export function buildSlots(
function buildDynamicSlot( function buildDynamicSlot(
name: ExpressionNode, name: ExpressionNode,
fn: FunctionExpression fn: FunctionExpression,
index?: number
): ObjectExpression { ): ObjectExpression {
return createObjectExpression([ const props = [
createObjectProperty(`name`, name), createObjectProperty(`name`, name),
createObjectProperty(`fn`, fn) createObjectProperty(`fn`, fn)
]) ]
if (index != null) {
props.push(
createObjectProperty(`key`, createSimpleExpression(String(index), true))
)
}
return createObjectExpression(props)
} }
function hasForwardedSlots(children: TemplateChildNode[]): boolean { function hasForwardedSlots(children: TemplateChildNode[]): boolean {

View File

@ -166,7 +166,8 @@ describe('ssr: components', () => {
_createTextVNode(\\"foo\\") _createTextVNode(\\"foo\\")
] ]
} }
}) }),
key: \\"0\\"
} }
: undefined : undefined
]), _parent)) ]), _parent))

View File

@ -17,6 +17,15 @@ describe('createSlot', () => {
expect(actual).toEqual({ descriptor: slot }) expect(actual).toEqual({ descriptor: slot })
}) })
it('should attach key', () => {
const dynamicSlot = [{ name: 'descriptor', fn: slot, key: '1' }]
const actual = createSlots(record, dynamicSlot)
const ret = actual.descriptor()
// @ts-ignore
expect(ret.key).toBe('1')
})
it('should add all slots to the record', () => { it('should add all slots to the record', () => {
const dynamicSlot = [ const dynamicSlot = [
{ name: 'descriptor', fn: slot }, { name: 'descriptor', fn: slot },

View File

@ -4,6 +4,7 @@ import { isArray } from '@vue/shared'
interface CompiledSlotDescriptor { interface CompiledSlotDescriptor {
name: string name: string
fn: Slot fn: Slot
key?: string
} }
/** /**
@ -27,7 +28,15 @@ export function createSlots(
} }
} else if (slot) { } else if (slot) {
// conditional single slot generated by <template v-if="..." #foo> // conditional single slot generated by <template v-if="..." #foo>
slots[slot.name] = slot.fn slots[slot.name] = slot.key
? (...args: any[]) => {
const res = slot.fn(...args)
// attach branch key so each conditional branch is considered a
// different fragment
;(res as any).key = slot.key
return res
}
: slot.fn
} }
} }
return slots return slots

View File

@ -66,7 +66,14 @@ export function renderSlot(
const validSlotContent = slot && ensureValidVNode(slot(props)) const validSlotContent = slot && ensureValidVNode(slot(props))
const rendered = createBlock( const rendered = createBlock(
Fragment, Fragment,
{ key: props.key || `_${name}` }, {
key:
props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`
},
validSlotContent || (fallback ? fallback() : []), validSlotContent || (fallback ? fallback() : []),
validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT ? PatchFlags.STABLE_FRAGMENT