diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index 16ea42ed3..dceda28fc 100644 --- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts +++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts @@ -1040,4 +1040,69 @@ describe('renderer: optimized mode', () => { expect(app.config.errorHandler).not.toHaveBeenCalled() } }) + + // #11336 + test('should bail manually rendered compiler slots for both mount and update (2)', async () => { + // only reproducible in prod + __DEV__ = false + const n = ref(0) + function Outer(_: any, { slots }: any) { + n.value // track + return slots.default() + } + const Mid = { + render(ctx: any) { + return ( + openBlock(), + createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')]) + ) + }, + } + const show = ref(false) + const App = { + render() { + return ( + openBlock(), + createBlock(Outer, null, { + default: withCtx(() => [ + createVNode(Mid, null, { + default: withCtx(() => [ + createElementVNode('div', null, [ + show.value + ? (openBlock(), + createElementBlock('div', { key: 0 }, '1')) + : createCommentVNode('v-if', true), + createElementVNode('div', null, '2'), + createElementVNode('div', null, '3'), + ]), + createElementVNode('div', null, '4'), + ]), + _: 1 /* STABLE */, + }), + ]), + _: 1 /* STABLE */, + }) + ) + }, + } + + const app = createApp(App) + app.config.errorHandler = vi.fn() + + try { + app.mount(root) + + // force Outer update, which will assign new slots to Mid + // we want to make sure the compiled slot flag doesn't accidentally + // get assigned again + n.value++ + await nextTick() + + show.value = true + await nextTick() + } finally { + __DEV__ = true + expect(app.config.errorHandler).not.toHaveBeenCalled() + } + }) }) diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 0145d557b..438c56efb 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -12,7 +12,6 @@ import { ShapeFlags, SlotFlags, def, - extend, isArray, isFunction, } from '@vue/shared' @@ -161,6 +160,22 @@ const normalizeVNodeSlots = ( instance.slots.default = () => normalized } +const assignSlots = ( + slots: InternalSlots, + children: Slots, + optimized: boolean, +) => { + for (const key in children) { + // #2893 + // when rendering the optimized slots by manually written render function, + // do not copy the `slots._` compiler flag so that `renderSlot` creates + // slot Fragment with BAIL patchFlag to force full updates + if (optimized || key !== '_') { + slots[key] = children[key] + } + } +} + export const initSlots = ( instance: ComponentInternalInstance, children: VNodeNormalizedChildren, @@ -170,16 +185,10 @@ export const initSlots = ( if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { const type = (children as RawSlots)._ if (type) { - extend(slots, children as InternalSlots) + assignSlots(slots, children as Slots, optimized) // make compiler marker non-enumerable if (optimized) { def(slots, '_', type, true) - } else { - // #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 - delete slots._ } } else { normalizeObjectSlots(children as RawSlots, slots, instance) @@ -204,7 +213,7 @@ export const updateSlots = ( if (__DEV__ && isHmrUpdating) { // 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) + assignSlots(slots, children as Slots, optimized) trigger(instance, TriggerOpTypes.SET, '$slots') } else if (optimized && type === SlotFlags.STABLE) { // compiled AND stable. @@ -213,7 +222,7 @@ export const updateSlots = ( } else { // compiled but dynamic (v-if/v-for on slots) - update slots, but skip // normalization. - extend(slots, children as Slots) + assignSlots(slots, children as Slots, optimized) } } else { needDeletionCheck = !(children as RawSlots).$stable