This commit is contained in:
edison 2025-07-01 11:08:38 +02:00 committed by GitHub
commit 6b5c0aae3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 77 additions and 1 deletions

View File

@ -418,6 +418,7 @@ export interface SuspenseBoundary {
container: RendererElement
hiddenContainer: RendererElement
activeBranch: VNode | null
initialContent: VNode | null
pendingBranch: VNode | null
deps: number
pendingId: number
@ -503,6 +504,7 @@ function createSuspenseBoundary(
pendingId: suspenseId++,
timeout: typeof timeout === 'number' ? timeout : -1,
activeBranch: null,
initialContent: null,
pendingBranch: null,
isInFallback: !isHydrating,
isHydrating,
@ -555,7 +557,11 @@ function createSuspenseBoundary(
}
}
// unmount current active tree
if (activeBranch) {
// #7966 when Suspense is wrapped in Transition, the fallback node will be mounted
// in the afterLeave of Transition. This means that when Suspense is resolved,
// the activeBranch is not the fallback node but the initialContent.
// so avoid unmounting the activeBranch again.
if (activeBranch && activeBranch !== suspense.initialContent) {
// if the fallback tree was mounted, it may have been moved
// as part of a parent suspense. get the latest anchor for insertion
// #8105 if `delayEnter` is true, it means that the mounting of
@ -632,6 +638,7 @@ function createSuspenseBoundary(
const anchor = next(activeBranch!)
const mountFallback = () => {
suspense.initialContent = null
if (!suspense.isInFallback) {
return
}
@ -653,6 +660,7 @@ function createSuspenseBoundary(
const delayEnter =
fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
if (delayEnter) {
suspense.initialContent = activeBranch!
activeBranch!.transition!.afterLeave = mountFallback
}
suspense.isInFallback = true

View File

@ -2007,6 +2007,74 @@ describe('e2e: Transition', () => {
E2E_TIMEOUT,
)
test(
'avoid unmount activeBranch twice with Suspense (out-in mode + timeout="0")',
async () => {
const unmountSpy = vi.fn()
await page().exposeFunction('unmountSpy', unmountSpy)
await page().evaluate(() => {
const { createApp, shallowRef, h } = (window as any).Vue
const One = {
setup() {
return () =>
h(
'div',
{
onVnodeBeforeUnmount: () => unmountSpy(),
},
'one',
)
},
}
const Two = {
async setup() {
return () => h('div', null, 'two')
},
}
createApp({
template: `
<div id="container">
<transition mode="out-in">
<suspense timeout="0">
<template #default>
<component :is="view"></component>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const view = shallowRef(One)
const click = () => {
view.value = view.value === One ? Two : One
}
return { view, click }
},
}).mount('#app')
})
expect(await html('#container')).toBe('<div>one</div>')
// leave
await classWhenTransitionStart()
await nextFrame()
expect(await html('#container')).toBe(
'<div class="v-enter-from v-enter-active">two</div>',
)
await transitionFinish()
expect(await html('#container')).toBe('<div class="">two</div>')
// should only call unmount once
expect(unmountSpy).toBeCalledTimes(1)
},
E2E_TIMEOUT,
)
// #5844
test('children mount should be called after html changes', async () => {
const fooMountSpy = vi.fn()