]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(suspense): avoid double-patching nested suspense when parent suspense is not...
authoredison <daiwei521@126.com>
Thu, 11 Jan 2024 09:27:53 +0000 (17:27 +0800)
committerGitHub <noreply@github.com>
Thu, 11 Jan 2024 09:27:53 +0000 (17:27 +0800)
close #8678

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

index 7c3f8ff8b3dc0d0bbede53edf2bd7b5a6ace0736..928da872faf4d56f01a94598364f20391af43357 100644 (file)
@@ -1641,6 +1641,141 @@ describe('Suspense', () => {
     expect(serializeInner(root)).toBe(expected)
   })
 
+  //#8678
+  test('nested suspense (child suspense update before parent suspense resolve)', async () => {
+    const calls: string[] = []
+
+    const InnerA = defineAsyncComponent(
+      {
+        setup: () => {
+          calls.push('innerA created')
+          onMounted(() => {
+            calls.push('innerA mounted')
+          })
+          return () => h('div', 'innerA')
+        },
+      },
+      10,
+    )
+
+    const InnerB = defineAsyncComponent(
+      {
+        setup: () => {
+          calls.push('innerB created')
+          onMounted(() => {
+            calls.push('innerB mounted')
+          })
+          return () => h('div', 'innerB')
+        },
+      },
+      10,
+    )
+
+    const OuterA = defineAsyncComponent(
+      {
+        setup: (_, { slots }: any) => {
+          calls.push('outerA created')
+          onMounted(() => {
+            calls.push('outerA mounted')
+          })
+          return () =>
+            h(Fragment, null, [h('div', 'outerA'), slots.default?.()])
+        },
+      },
+      5,
+    )
+
+    const OuterB = defineAsyncComponent(
+      {
+        setup: (_, { slots }: any) => {
+          calls.push('outerB created')
+          onMounted(() => {
+            calls.push('outerB mounted')
+          })
+          return () =>
+            h(Fragment, null, [h('div', 'outerB'), slots.default?.()])
+        },
+      },
+      5,
+    )
+
+    const outerToggle = ref(false)
+    const innerToggle = ref(false)
+
+    /**
+     *  <Suspense>
+     *    <component :is="outerToggle ? outerB : outerA">
+     *      <Suspense>
+     *        <component :is="innerToggle ? innerB : innerA" />
+     *      </Suspense>
+     *    </component>
+     *  </Suspense>
+     */
+    const Comp = {
+      setup() {
+        return () =>
+          h(Suspense, null, {
+            default: [
+              h(outerToggle.value ? OuterB : OuterA, null, {
+                default: () =>
+                  h(Suspense, null, {
+                    default: h(innerToggle.value ? InnerB : InnerA),
+                  }),
+              }),
+            ],
+            fallback: h('div', 'fallback outer'),
+          })
+      },
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(Comp), root)
+    expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
+
+    // mount outer component
+    await Promise.all(deps)
+    await nextTick()
+
+    expect(serializeInner(root)).toBe(`<div>outerA</div><!---->`)
+    expect(calls).toEqual([`outerA created`, `outerA mounted`])
+
+    // mount inner component
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>outerA</div><div>innerA</div>`)
+
+    expect(calls).toEqual([
+      'outerA created',
+      'outerA mounted',
+      'innerA created',
+      'innerA mounted',
+    ])
+
+    calls.length = 0
+    deps.length = 0
+
+    // toggle both outer and inner components
+    outerToggle.value = true
+    innerToggle.value = true
+    await nextTick()
+
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>outerB</div><!---->`)
+
+    await Promise.all(deps)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>outerB</div><div>innerB</div>`)
+
+    // innerB only mount once
+    expect(calls).toEqual([
+      'outerB created',
+      'outerB mounted',
+      'innerB created',
+      'innerB mounted',
+    ])
+  })
+
   // #6416
   test('KeepAlive with Suspense', async () => {
     const Async = defineAsyncComponent({
index ac023ae60cd679af78fff164845fca88d6641362..9b3d6765d9c0b305b32cd32303786634172aa919 100644 (file)
@@ -91,6 +91,18 @@ export const SuspenseImpl = {
         rendererInternals,
       )
     } else {
+      // #8678 if the current suspense needs to be patched and parentSuspense has
+      // not been resolved. this means that both the current suspense and parentSuspense
+      // need to be patched. because parentSuspense's pendingBranch includes the
+      // current suspense, it will be processed twice:
+      //  1. current patch
+      //  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) {
+        n2.suspense = n1.suspense
+        return
+      }
       patchSuspense(
         n1,
         n2,