Skip to content

Commit aab99ab

Browse files
committed
fix(slots): properly force update on forwarded slots
fix #1594
1 parent 44e6da1 commit aab99ab

File tree

7 files changed

+99
-22
lines changed

7 files changed

+99
-22
lines changed

packages/compiler-core/__tests__/transforms/vSlot.spec.ts

+17
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,23 @@ describe('compiler: transform component slots', () => {
719719
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
720720
})
721721

722+
test('generate flag on forwarded slots', () => {
723+
const { slots } = parseWithSlots(`<Comp><slot/></Comp>`)
724+
expect(slots).toMatchObject({
725+
type: NodeTypes.JS_OBJECT_EXPRESSION,
726+
properties: [
727+
{
728+
key: { content: `default` },
729+
value: { type: NodeTypes.JS_FUNCTION_EXPRESSION }
730+
},
731+
{
732+
key: { content: `_` },
733+
value: { content: `3` } // forwarded
734+
}
735+
]
736+
})
737+
})
738+
722739
describe('errors', () => {
723740
test('error on extraneous children w/ named default slot', () => {
724741
const onError = jest.fn()

packages/compiler-core/src/transforms/vSlot.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from '../utils'
3434
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
3535
import { parseForExpression, createForLoopParams } from './vFor'
36+
import { SlotFlags } from '@vue/shared/src'
3637

3738
const defaultFallback = createSimpleExpression(`undefined`, false)
3839

@@ -321,13 +322,19 @@ export function buildSlots(
321322
}
322323
}
323324

325+
const slotFlag = hasDynamicSlots
326+
? SlotFlags.DYNAMIC
327+
: hasForwardedSlots(node.children)
328+
? SlotFlags.FORWARDED
329+
: SlotFlags.STABLE
330+
324331
let slots = createObjectExpression(
325332
slotsProperties.concat(
326333
createObjectProperty(
327334
`_`,
328335
// 2 = compiled but dynamic = can skip normalization, but must run diff
329336
// 1 = compiled and static = can skip normalization AND diff as optimized
330-
createSimpleExpression(hasDynamicSlots ? `2` : `1`, false)
337+
createSimpleExpression('' + slotFlag, false)
331338
)
332339
),
333340
loc
@@ -354,3 +361,19 @@ function buildDynamicSlot(
354361
createObjectProperty(`fn`, fn)
355362
])
356363
}
364+
365+
function hasForwardedSlots(children: TemplateChildNode[]): boolean {
366+
for (let i = 0; i < children.length; i++) {
367+
const child = children[i]
368+
if (child.type === NodeTypes.ELEMENT) {
369+
if (
370+
child.tagType === ElementTypes.SLOT ||
371+
(child.tagType === ElementTypes.ELEMENT &&
372+
hasForwardedSlots(child.children))
373+
) {
374+
return true
375+
}
376+
}
377+
}
378+
return false
379+
}

packages/runtime-core/src/componentSlots.ts

+18-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
EMPTY_OBJ,
1313
ShapeFlags,
1414
extend,
15-
def
15+
def,
16+
SlotFlags
1617
} from '@vue/shared'
1718
import { warn } from './warning'
1819
import { isKeepAlive } from './components/KeepAlive'
@@ -27,24 +28,25 @@ export type InternalSlots = {
2728

2829
export type Slots = Readonly<InternalSlots>
2930

30-
export const enum CompiledSlotTypes {
31-
STATIC = 1,
32-
DYNAMIC = 2
33-
}
34-
3531
export type RawSlots = {
3632
[name: string]: unknown
3733
// manual render fn hint to skip forced children updates
3834
$stable?: boolean
39-
// internal, for tracking slot owner instance. This is attached during
40-
// normalizeChildren when the component vnode is created.
35+
/**
36+
* for tracking slot owner instance. This is attached during
37+
* normalizeChildren when the component vnode is created.
38+
* @internal
39+
*/
4140
_ctx?: ComponentInternalInstance | null
42-
// internal, indicates compiler generated slots
43-
// we use a reserved property instead of a vnode patchFlag because the slots
44-
// object may be directly passed down to a child component in a manual
45-
// render function, and the optimization hint need to be on the slot object
46-
// itself to be preserved.
47-
_?: CompiledSlotTypes
41+
/**
42+
* indicates compiler generated slots
43+
* we use a reserved property instead of a vnode patchFlag because the slots
44+
* object may be directly passed down to a child component in a manual
45+
* render function, and the optimization hint need to be on the slot object
46+
* itself to be preserved.
47+
* @internal
48+
*/
49+
_?: SlotFlags
4850
}
4951

5052
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
@@ -141,8 +143,8 @@ export const updateSlots = (
141143
// Parent was HMR updated so slot content may have changed.
142144
// force update slots and mark instance for hmr as well
143145
extend(slots, children as Slots)
144-
} else if (type === CompiledSlotTypes.STATIC) {
145-
// compiled AND static.
146+
} else if (type === SlotFlags.STABLE) {
147+
// compiled AND stable.
146148
// no need to update, and skip stale slots removal.
147149
needDeletionCheck = false
148150
} else {

packages/runtime-core/src/helpers/renderSlot.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Data } from '../component'
2-
import { Slots, RawSlots, CompiledSlotTypes } from '../componentSlots'
2+
import { Slots, RawSlots } from '../componentSlots'
33
import {
44
VNodeArrayChildren,
55
openBlock,
66
createBlock,
77
Fragment,
88
VNode
99
} from '../vnode'
10-
import { PatchFlags } from '@vue/shared'
10+
import { PatchFlags, SlotFlags } from '@vue/shared'
1111
import { warn } from '../warning'
1212

1313
/**
@@ -39,7 +39,7 @@ export function renderSlot(
3939
Fragment,
4040
{ key: props.key },
4141
slot ? slot(props) : fallback ? fallback() : [],
42-
(slots as RawSlots)._ === CompiledSlotTypes.STATIC
42+
(slots as RawSlots)._ === SlotFlags.STABLE
4343
? PatchFlags.STABLE_FRAGMENT
4444
: PatchFlags.BAIL
4545
)

packages/runtime-core/src/vnode.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
normalizeClass,
99
normalizeStyle,
1010
PatchFlags,
11-
ShapeFlags
11+
ShapeFlags,
12+
SlotFlags
1213
} from '@vue/shared'
1314
import {
1415
ComponentInternalInstance,
@@ -542,10 +543,22 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
542543
return
543544
} else {
544545
type = ShapeFlags.SLOTS_CHILDREN
545-
if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) {
546+
const slotFlag = (children as RawSlots)._
547+
if (!slotFlag && !(InternalObjectKey in children!)) {
546548
// if slots are not normalized, attach context instance
547549
// (compiled / normalized slots already have context)
548550
;(children as RawSlots)._ctx = currentRenderingInstance
551+
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
552+
// a child component receives forwarded slots from the parent.
553+
// its slot type is determined by its parent's slot type.
554+
if (
555+
currentRenderingInstance.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS
556+
) {
557+
;(children as RawSlots)._ = SlotFlags.DYNAMIC
558+
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
559+
} else {
560+
;(children as RawSlots)._ = SlotFlags.STABLE
561+
}
549562
}
550563
}
551564
} else if (isFunction(children)) {

packages/shared/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { makeMap } from './makeMap'
33
export { makeMap }
44
export * from './patchFlags'
55
export * from './shapeFlags'
6+
export * from './slotFlags'
67
export * from './globalsWhitelist'
78
export * from './codeframe'
89
export * from './mockWarn'

packages/shared/src/slotFlags.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const enum SlotFlags {
2+
/**
3+
* Stable slots that only reference slot props or context state. The slot
4+
* can fully capture its own dependencies so when passed down the parent won't
5+
* need to force the child to update.
6+
*/
7+
STABLE = 1,
8+
/**
9+
* Slots that reference scope variables (v-for or an outer slot prop), or
10+
* has conditional structure (v-if, v-for). The parent will need to force
11+
* the child to update because the slot does not fully capture its dependencies.
12+
*/
13+
DYNAMIC = 2,
14+
/**
15+
* <slot/> being forwarded into a child component. Whether the parent needs
16+
* to update the child is dependent on what kind of slots the parent itself
17+
* received. This has to be refined at runtime, when the child's vnode
18+
* is being created (in `normalizeChildren`)
19+
*/
20+
FORWARDED = 3
21+
}

0 commit comments

Comments
 (0)