From 685e3f381c024b9f4023e60fe0545dc60d90d984 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 12 Jul 2024 17:29:42 +0800 Subject: [PATCH] fix(runtime-core): more edge case fix for manually rendered compiled slot close #11336 --- .../__tests__/rendererOptimizedMode.spec.ts | 65 +++++++++++++++++++ packages/runtime-core/src/componentSlots.ts | 29 ++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index 16ea42ed342..dceda28fcae 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 0145d557b06..438c56efb47 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