]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(suspense): ensure nested suspense patching if in fallback state (#10417)
authoredison <daiwei521@126.com>
Wed, 28 Feb 2024 11:05:22 +0000 (19:05 +0800)
committerGitHub <noreply@github.com>
Wed, 28 Feb 2024 11:05:22 +0000 (19:05 +0800)
close #10415

packages/runtime-core/__tests__/components/Suspense.spec.ts
packages/runtime-core/src/components/Suspense.ts

index fd1913b2c9c307c6746defacf9caf1b8541cf441..a448972e1396830ac00ae78c6626e89fbe36b7a9 100644 (file)
@@ -54,6 +54,18 @@ describe('Suspense', () => {
     }
   }
 
+  const RouterView = {
+    setup(_: any, { slots }: any) {
+      const route = inject('route') as any
+      const depth = inject('depth', 0)
+      provide('depth', depth + 1)
+      return () => {
+        const current = route.value[depth]
+        return slots.default({ Component: current })[0]
+      }
+    },
+  }
+
   test('fallback content', async () => {
     const Async = defineAsyncComponent({
       render() {
@@ -1041,18 +1053,6 @@ describe('Suspense', () => {
 
   // #10098
   test('switching branches w/ nested suspense', async () => {
-    const RouterView = {
-      setup(_: any, { slots }: any) {
-        const route = inject('route') as any
-        const depth = inject('depth', 0)
-        provide('depth', depth + 1)
-        return () => {
-          const current = route.value[depth]
-          return slots.default({ Component: current })[0]
-        }
-      },
-    }
-
     const OuterB = defineAsyncComponent({
       setup: () => {
         return () =>
@@ -1132,6 +1132,121 @@ describe('Suspense', () => {
     expect(serializeInner(root)).toBe(`<div>innerA</div>`)
   })
 
+  // #10415
+  test('nested suspense (w/ suspensible) switch several times before parent suspense resolve', async () => {
+    const OuterA = defineAsyncComponent({
+      setup: () => {
+        return () =>
+          h(RouterView, null, {
+            default: ({ Component }: any) => [
+              h(Suspense, null, {
+                default: () => h(Component),
+              }),
+            ],
+          })
+      },
+    })
+
+    const InnerA = defineAsyncComponent({
+      setup: () => {
+        return () => h('div', 'innerA')
+      },
+    })
+
+    const route = shallowRef([OuterA, InnerA])
+    const InnerB = defineAsyncComponent(
+      {
+        setup: () => {
+          return () => h('div', 'innerB')
+        },
+      },
+      5,
+    )
+
+    const InnerB1 = defineAsyncComponent(
+      {
+        setup: () => {
+          return () => h('div', 'innerB1')
+        },
+      },
+      5,
+    )
+
+    const InnerB2 = defineAsyncComponent(
+      {
+        setup: () => {
+          return () => h('div', 'innerB2')
+        },
+      },
+      5,
+    )
+
+    const OuterB = defineAsyncComponent(
+      {
+        setup() {
+          nextTick(async () => {
+            await new Promise(resolve => setTimeout(resolve, 1))
+            route.value = [OuterB, InnerB1]
+          })
+
+          nextTick(async () => {
+            await new Promise(resolve => setTimeout(resolve, 1))
+            route.value = [OuterB, InnerB2]
+          })
+
+          return () =>
+            h(RouterView, null, {
+              default: ({ Component }: any) => [
+                h(
+                  Suspense,
+                  { suspensible: true },
+                  {
+                    default: () => h(Component),
+                  },
+                ),
+              ],
+            })
+        },
+      },
+      5,
+    )
+
+    const Comp = {
+      setup() {
+        provide('route', route)
+        return () =>
+          h(RouterView, null, {
+            default: ({ Component }: any) => [
+              h(Suspense, null, {
+                default: () => h(Component),
+              }),
+            ],
+          })
+      },
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(Comp), root)
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>innerA</div>`)
+
+    deps.length = 0
+
+    route.value = [OuterB, InnerB]
+    await nextTick()
+
+    await Promise.all(deps)
+    await Promise.all(deps)
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>innerB2</div>`)
+  })
+
   test('branch switch to 3rd branch before resolve', async () => {
     const calls: string[] = []
 
index 65b05c3dd7c1a3f9a0ad7dc3b67d1ac51be84415..9461d7fec978b9bab3b2b6ed991341092f2cf340 100644 (file)
@@ -99,7 +99,11 @@ export const SuspenseImpl = {
       //  2. mounting along with the pendingBranch of parentSuspense
       // it is necessary to skip the current patch to avoid multiple mounts
       // of inner components.
-      if (parentSuspense && parentSuspense.deps > 0) {
+      if (
+        parentSuspense &&
+        parentSuspense.deps > 0 &&
+        !n1.suspense!.isInFallback
+      ) {
         n2.suspense = n1.suspense!
         n2.suspense.vnode = n2
         n2.el = n1.el