fix(runtime-core): more edge case fix for manually rendered compiled slot

close #11336
This commit is contained in:
Evan You 2024-07-12 17:29:42 +08:00
parent 314ce82e47
commit 685e3f381c
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
2 changed files with 84 additions and 10 deletions

View File

@ -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()
}
})
})

View File

@ -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