diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index 24f496ca..3c653281 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -15,7 +15,10 @@ import {
defineComponent,
withCtx,
renderSlot,
- onBeforeUnmount
+ onBeforeUnmount,
+ createTextVNode,
+ SetupContext,
+ createApp
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
@@ -517,4 +520,60 @@ describe('renderer: optimized mode', () => {
expect(spyA).toHaveBeenCalledTimes(1)
expect(spyB).toHaveBeenCalledTimes(1)
})
+
+ // #2893
+ test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
+ const state = ref(0)
+
+ const CompA = {
+ setup(props: any, { slots }: SetupContext) {
+ return () => {
+ return (
+ openBlock(),
+ createBlock('div', null, [renderSlot(slots, 'default')])
+ )
+ }
+ }
+ }
+
+ const Wrapper = {
+ setup(props: any, { slots }: SetupContext) {
+ // use the manually written render function to rendering the optimized slots,
+ // which should make subsequent updates exit the optimized mode correctly
+ return () => {
+ return slots.default!()[state.value]
+ }
+ }
+ }
+
+ const app = createApp({
+ setup() {
+ return () => {
+ return (
+ openBlock(),
+ createBlock(Wrapper, null, {
+ default: withCtx(() => [
+ createVNode(CompA, null, {
+ default: withCtx(() => [createTextVNode('Hello')]),
+ _: 1 /* STABLE */
+ }),
+ createVNode(CompA, null, {
+ default: withCtx(() => [createTextVNode('World')]),
+ _: 1 /* STABLE */
+ })
+ ]),
+ _: 1 /* STABLE */
+ })
+ )
+ }
+ }
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe('
Hello
')
+
+ state.value = 1
+ await nextTick()
+ expect(inner(root)).toBe('World
')
+ })
})
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 325f076f..f1eacd87 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -130,7 +130,8 @@ export const initSlots = (
export const updateSlots = (
instance: ComponentInternalInstance,
- children: VNodeNormalizedChildren
+ children: VNodeNormalizedChildren,
+ optimized: boolean
) => {
const { vnode, slots } = instance
let needDeletionCheck = true
@@ -143,7 +144,7 @@ export const updateSlots = (
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
extend(slots, children as Slots)
- } else if (type === SlotFlags.STABLE) {
+ } else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
// no need to update, and skip stale slots removal.
needDeletionCheck = false
@@ -151,6 +152,13 @@ export const updateSlots = (
// compiled but dynamic (v-if/v-for on slots) - update slots, but skip
// normalization.
extend(slots, children as Slots)
+ // #2893
+ // when rendering the optimized slots by manually written render function,
+ // we need to delete the `slots._` flag if necessary to make subsequent updates reliable,
+ // i.e. let the `renderSlot` create the bailed Fragment
+ if (!optimized && type === SlotFlags.STABLE) {
+ delete slots._
+ }
}
} else {
needDeletionCheck = !(children as RawSlots).$stable
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 15f86faa..2c1b2bbd 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1576,7 +1576,7 @@ function baseCreateRenderer(
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode.props, prevProps, optimized)
- updateSlots(instance, nextVNode.children)
+ updateSlots(instance, nextVNode.children, optimized)
pauseTracking()
// props update may have triggered pre-flush watchers.