From f435e19cd94ec0f9898ebff05e4a3d066abfa855 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 13 Oct 2023 09:30:43 +0800 Subject: [PATCH 1/6] fix(Suspense): avoid unmount activeBranch twice if wrapped in transtion --- packages/runtime-core/src/components/Suspense.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 0fa07d9beec..ba4110d09cd 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -379,7 +379,7 @@ export interface SuspenseBoundary { container: RendererElement hiddenContainer: RendererElement anchor: RendererNode | null - activeBranch: VNode | null + activeBranch: (VNode & { __isUnmounted?: boolean }) | null pendingBranch: VNode | null deps: number pendingId: number @@ -508,7 +508,10 @@ function createSuspenseBoundary( // this is initial anchor on mount let { anchor } = suspense // unmount current active tree - if (activeBranch) { + // #7966 if suspense is wrapped in Transition, the Transition's afterLeave may not have been + // performed (this means the fallbackVNode not mounted) when suspense resolves. + // so avoid unmount activeBranch again + if (activeBranch && !activeBranch.__isUnmounted) { // if the fallback tree was mounted, it may have been moved // as part of a parent suspense. get the latest anchor for insertion anchor = next(activeBranch) @@ -608,6 +611,8 @@ function createSuspenseBoundary( true // shouldRemove ) + activeBranch!.__isUnmounted = true + if (!delayEnter) { mountFallback() } From c9f53ccda410709e12eee1908bf1313ef7882bac Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 13 Oct 2023 16:57:35 +0800 Subject: [PATCH 2/6] test: add test case --- packages/vue/__tests__/e2e/Transition.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 326eaa57e3d..dc90c490793 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1498,6 +1498,76 @@ 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: ` +
+ + + + + + +
+ + `, + setup: () => { + const view = shallowRef(One) + const click = () => { + view.value = view.value === One ? Two : One + } + return { view, click } + } + }).mount('#app') + }) + + expect(await html('#container')).toBe('
one
') + + // leave + await classWhenTransitionStart() + await nextFrame() + expect(await html('#container')).toBe( + '
two
' + ) + + await transitionFinish() + expect(await html('#container')).toBe('
two
') + + // should only call unmount once + expect(unmountSpy).toBeCalledTimes(1) + }, + E2E_TIMEOUT + ) }) describe('transition with v-show', () => { From 9c382fcc6099f70a0c178fc7ac0a04c8222d33c9 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 13 Oct 2023 16:59:45 +0800 Subject: [PATCH 3/6] test: add test case chore: improve code chore: improve code chore: improve code chore(deps): update dependency @types/node to v18 (#9323) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> chore: improve code chore: revert code chore: improve code --- .../runtime-core/src/components/Suspense.ts | 19 +++++++------ packages/vue/__tests__/e2e/Transition.spec.ts | 28 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index ba4110d09cd..4fdf11fed0b 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -379,7 +379,8 @@ export interface SuspenseBoundary { container: RendererElement hiddenContainer: RendererElement anchor: RendererNode | null - activeBranch: (VNode & { __isUnmounted?: boolean }) | null + activeBranch: VNode | null + initialContent: VNode | null pendingBranch: VNode | null deps: number pendingId: number @@ -462,6 +463,7 @@ function createSuspenseBoundary( pendingId: 0, timeout: typeof timeout === 'number' ? timeout : -1, activeBranch: null, + initialContent: null, pendingBranch: null, isInFallback: true, isHydrating, @@ -506,12 +508,13 @@ function createSuspenseBoundary( } } // this is initial anchor on mount - let { anchor } = suspense + let { anchor, initialContent } = suspense // unmount current active tree - // #7966 if suspense is wrapped in Transition, the Transition's afterLeave may not have been - // performed (this means the fallbackVNode not mounted) when suspense resolves. - // so avoid unmount activeBranch again - if (activeBranch && !activeBranch.__isUnmounted) { + // #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 activateBranch again. + if (activeBranch && activeBranch !== initialContent) { // if the fallback tree was mounted, it may have been moved // as part of a parent suspense. get the latest anchor for insertion anchor = next(activeBranch) @@ -578,6 +581,7 @@ function createSuspenseBoundary( const anchor = next(activeBranch!) const mountFallback = () => { + suspense.initialContent = null if (!suspense.isInFallback) { return } @@ -599,6 +603,7 @@ function createSuspenseBoundary( const delayEnter = fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in' if (delayEnter) { + suspense.initialContent = activeBranch! activeBranch!.transition!.afterLeave = mountFallback } suspense.isInFallback = true @@ -611,8 +616,6 @@ function createSuspenseBoundary( true // shouldRemove ) - activeBranch!.__isUnmounted = true - if (!delayEnter) { mountFallback() } diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index dc90c490793..4d682c97b66 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1503,9 +1503,7 @@ describe('e2e: Transition', () => { '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 = { @@ -1527,19 +1525,19 @@ describe('e2e: Transition', () => { } createApp({ template: ` -
- - - - - - -
- +
+ + + + + + +
+ `, setup: () => { const view = shallowRef(One) From 9e436aff6e9e10aa4311e90cc6445b12c7be88fd Mon Sep 17 00:00:00 2001 From: edison1105 Date: Sat, 14 Oct 2023 16:36:42 +0800 Subject: [PATCH 4/6] fix: typo --- packages/runtime-core/src/components/Suspense.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 4fdf11fed0b..e4c81b48870 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -513,7 +513,7 @@ function createSuspenseBoundary( // #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 activateBranch again. + // so avoid unmounting the activeBranch again. if (activeBranch && activeBranch !== initialContent) { // if the fallback tree was mounted, it may have been moved // as part of a parent suspense. get the latest anchor for insertion From 514abb50efa2da4b42bfafc3bd83699acf793be2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 28 Oct 2023 08:21:57 +0000 Subject: [PATCH 5/6] [autofix.ci] apply automated fixes --- packages/vue/__tests__/e2e/Transition.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 563bb455c57..f88f2f3893e 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1566,7 +1566,7 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT ) - + // #5844 test('children mount should be called after html changes', async () => { const fooMountSpy = vi.fn() From 41f25fb885505d2465e0afe8096a7f7a1ed2eeea Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 06:07:39 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes --- packages/vue/__tests__/e2e/Transition.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 3a563feb7be..f6031e954b9 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1512,16 +1512,16 @@ describe('e2e: Transition', () => { h( 'div', { - onVnodeBeforeUnmount: () => unmountSpy() + onVnodeBeforeUnmount: () => unmountSpy(), }, - 'one' + 'one', ) - } + }, } const Two = { async setup() { return () => h('div', null, 'two') - } + }, } createApp({ template: ` @@ -1545,7 +1545,7 @@ describe('e2e: Transition', () => { view.value = view.value === One ? Two : One } return { view, click } - } + }, }).mount('#app') }) @@ -1555,7 +1555,7 @@ describe('e2e: Transition', () => { await classWhenTransitionStart() await nextFrame() expect(await html('#container')).toBe( - '
two
' + '
two
', ) await transitionFinish() @@ -1564,7 +1564,7 @@ describe('e2e: Transition', () => { // should only call unmount once expect(unmountSpy).toBeCalledTimes(1) }, - E2E_TIMEOUT + E2E_TIMEOUT, ) // #5844