Просмотр исходного кода

fix(suspense): fix anchor for suspense with transition out-in (#9999)

close #9996
白雾三语 2 лет назад
Родитель
Сommit
a3fbf2132b

+ 13 - 6
packages/runtime-core/src/components/Suspense.ts

@@ -400,7 +400,6 @@ export interface SuspenseBoundary {
   namespace: ElementNamespace
   container: RendererElement
   hiddenContainer: RendererElement
-  anchor: RendererNode | null
   activeBranch: VNode | null
   pendingBranch: VNode | null
   deps: number
@@ -473,6 +472,7 @@ function createSuspenseBoundary(
     assertNumber(timeout, `Suspense timeout`)
   }
 
+  const initialAnchor = anchor
   const suspense: SuspenseBoundary = {
     vnode,
     parent: parentSuspense,
@@ -480,7 +480,6 @@ function createSuspenseBoundary(
     namespace,
     container,
     hiddenContainer,
-    anchor,
     deps: 0,
     pendingId: suspenseId++,
     timeout: typeof timeout === 'number' ? timeout : -1,
@@ -529,20 +528,28 @@ function createSuspenseBoundary(
               move(
                 pendingBranch!,
                 container,
-                next(activeBranch!),
+                anchor === initialAnchor ? next(activeBranch!) : anchor,
                 MoveType.ENTER,
               )
               queuePostFlushCb(effects)
             }
           }
         }
-        // this is initial anchor on mount
-        let { anchor } = suspense
         // unmount current active tree
         if (activeBranch) {
           // 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)
+          // #8105 if `delayEnter` is true, it means that the mounting of
+          // `activeBranch` will be delayed. if the branch switches before
+          // transition completes, both `activeBranch` and `pendingBranch` may
+          // coexist in the `hiddenContainer`. This could result in
+          // `next(activeBranch!)` obtaining an incorrect anchor
+          // (got `pendingBranch.el`).
+          // Therefore, after the mounting of activeBranch is completed,
+          // it is necessary to get the latest anchor.
+          if (parentNode(activeBranch.el!) !== suspense.hiddenContainer) {
+            anchor = next(activeBranch)
+          }
           unmount(activeBranch, parentComponent, suspense, true)
         }
         if (!delayEnter) {

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

@@ -1652,6 +1652,77 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+
+    // #9996
+    test(
+      'trigger again when transition is not finished & correctly anchor',
+      async () => {
+        await page().evaluate(duration => {
+          const { createApp, shallowRef, h } = (window as any).Vue
+          const One = {
+            async setup() {
+              return () => h('div', { class: 'test' }, 'one')
+            },
+          }
+          const Two = {
+            async setup() {
+              return () => h('div', { class: 'test' }, 'two')
+            },
+          }
+          createApp({
+            template: `
+            <div id="container">
+              <div>Top</div>
+              <transition name="test" mode="out-in" :duration="${duration}">
+                <Suspense>
+                  <component :is="view"/>
+                </Suspense>
+              </transition>
+              <div>Bottom</div>
+            </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')
+        }, duration)
+
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-enter-active test-enter-to">one</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test">one</div><div>Bottom</div>',
+        )
+
+        // trigger twice
+        classWhenTransitionStart()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-leave-active test-leave-to">one</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-enter-active test-enter-to">two</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test">two</div><div>Bottom</div>',
+        )
+      },
+      E2E_TIMEOUT,
+    )
   })
 
   describe('transition with v-show', () => {