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.