Преглед изворни кода

fix(suspense): avoid unmount activeBranch twice if wrapped in transition (#9392)

close #7966
edison пре 1 месец
родитељ
комит
908c6ad05e

+ 9 - 1
packages/runtime-core/src/components/Suspense.ts

@@ -423,6 +423,7 @@ export interface SuspenseBoundary {
   container: RendererElement
   hiddenContainer: RendererElement
   activeBranch: VNode | null
+  isFallbackMountPending: boolean
   pendingBranch: VNode | null
   deps: number
   pendingId: number
@@ -508,6 +509,7 @@ function createSuspenseBoundary(
     pendingId: suspenseId++,
     timeout: typeof timeout === 'number' ? timeout : -1,
     activeBranch: null,
+    isFallbackMountPending: false,
     pendingBranch: null,
     isInFallback: !isHydrating,
     isHydrating,
@@ -565,7 +567,10 @@ function createSuspenseBoundary(
           }
         }
         // unmount current active tree
-        if (activeBranch) {
+        // #7966 when Suspense is wrapped in Transition, fallback may wait for
+        // afterLeave before mounting. In that window, activeBranch is still the
+        // leaving content, so avoid unmounting it again during resolve.
+        if (activeBranch && !suspense.isFallbackMountPending) {
           // 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
@@ -591,6 +596,7 @@ function createSuspenseBoundary(
         }
       }
 
+      suspense.isFallbackMountPending = false
       setActiveBranch(suspense, pendingBranch!)
       suspense.pendingBranch = null
       suspense.isInFallback = false
@@ -646,6 +652,7 @@ function createSuspenseBoundary(
 
       const anchor = next(activeBranch!)
       const mountFallback = () => {
+        suspense.isFallbackMountPending = false
         if (!suspense.isInFallback) {
           return
         }
@@ -667,6 +674,7 @@ function createSuspenseBoundary(
       const delayEnter =
         fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
       if (delayEnter) {
+        suspense.isFallbackMountPending = true
         activeBranch!.transition!.afterLeave = mountFallback
       }
       suspense.isInFallback = true

+ 68 - 0
packages/vue/__tests__/e2e/Transition.spec.ts

@@ -2177,6 +2177,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()