]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(suspense): fix nested suspensible suspense with no asyn deps
authorEvan You <yyx990803@gmail.com>
Mon, 8 May 2023 08:37:46 +0000 (16:37 +0800)
committerEvan You <yyx990803@gmail.com>
Mon, 8 May 2023 08:37:46 +0000 (16:37 +0800)
close #8206

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

index 80d213fc790acf7f7821a7eaa2534cbc1e3bc91f..2942624dd1024e9de118685c65c9f2392704375f 100644 (file)
@@ -19,7 +19,7 @@ import {
   shallowRef,
   Fragment
 } from '@vue/runtime-test'
-import { createApp } from 'vue'
+import { createApp, defineComponent } from 'vue'
 
 describe('Suspense', () => {
   const deps: Promise<any>[] = []
@@ -1335,9 +1335,14 @@ describe('Suspense', () => {
           h(Suspense, null, {
             default: [
               h(outerToggle.value ? OuterB : OuterA, null, {
-                default: () => h(Suspense, { suspensible: true },{
-                  default: h(innerToggle.value ? InnerB : InnerA)
-                })
+                default: () =>
+                  h(
+                    Suspense,
+                    { suspensible: true },
+                    {
+                      default: h(innerToggle.value ? InnerB : InnerA)
+                    }
+                  )
               })
             ],
             fallback: h('div', 'fallback outer')
@@ -1400,4 +1405,122 @@ describe('Suspense', () => {
     expect(serializeInner(root)).toBe(expected)
     expect(calls).toContain('innerB mounted')
   })
+
+  // #8206
+  test('nested suspense with suspensible & no async deps', async () => {
+    const calls: string[] = []
+    let expected = ''
+
+    const InnerA = defineComponent({
+      setup: () => {
+        calls.push('innerA created')
+        onMounted(() => {
+          calls.push('innerA mounted')
+        })
+        return () => h('div', 'innerA')
+      }
+    })
+
+    const InnerB = defineComponent({
+      setup: () => {
+        calls.push('innerB created')
+        onMounted(() => {
+          calls.push('innerB mounted')
+        })
+        return () => h('div', 'innerB')
+      }
+    })
+
+    const OuterA = defineComponent({
+      setup: (_, { slots }: any) => {
+        calls.push('outerA created')
+        onMounted(() => {
+          calls.push('outerA mounted')
+        })
+        return () => h(Fragment, null, [h('div', 'outerA'), slots.default?.()])
+      }
+    })
+
+    const OuterB = defineComponent({
+      setup: (_, { slots }: any) => {
+        calls.push('outerB created')
+        onMounted(() => {
+          calls.push('outerB mounted')
+        })
+        return () => h(Fragment, null, [h('div', 'outerB'), slots.default?.()])
+      }
+    })
+
+    const outerToggle = ref(false)
+    const innerToggle = ref(false)
+
+    /**
+     *  <Suspense>
+     *    <component :is="outerToggle ? outerB : outerA">
+     *      <Suspense suspensible>
+     *        <component :is="innerToggle ? innerB : innerA" />
+     *      </Suspense>
+     *    </component>
+     *  </Suspense>
+     */
+    const Comp = defineComponent({
+      setup() {
+        return () =>
+          h(Suspense, null, {
+            default: [
+              h(outerToggle.value ? OuterB : OuterA, null, {
+                default: () =>
+                  h(
+                    Suspense,
+                    { suspensible: true },
+                    {
+                      default: h(innerToggle.value ? InnerB : InnerA)
+                    }
+                  )
+              })
+            ],
+            fallback: h('div', 'fallback outer')
+          })
+      }
+    })
+
+    expected = `<div>outerA</div><div>innerA</div>`
+    const root = nodeOps.createElement('div')
+    render(h(Comp), root)
+    expect(serializeInner(root)).toBe(expected)
+
+    // mount outer component and inner component
+    await Promise.all(deps)
+    await nextTick()
+
+    expect(serializeInner(root)).toBe(expected)
+    expect(calls).toEqual([
+      'outerA created',
+      'innerA created',
+      'innerA mounted',
+      'outerA mounted'
+    ])
+
+    // toggle outer component
+    calls.length = 0
+    deps.length = 0
+    outerToggle.value = true
+    await nextTick()
+
+    await Promise.all(deps)
+    await nextTick()
+    expected = `<div>outerB</div><div>innerA</div>`
+    expect(serializeInner(root)).toBe(expected)
+    expect(calls).toContain('outerB mounted')
+    expect(calls).toContain('innerA mounted')
+
+    // toggle inner component
+    calls.length = 0
+    deps.length = 0
+    innerToggle.value = true
+    await Promise.all(deps)
+    await nextTick()
+    expected = `<div>outerB</div><div>innerB</div>`
+    expect(serializeInner(root)).toBe(expected)
+  })
 })
index fd6a7e0fac8dc213038c1846f5094a9cc970d016..174f33e13ad6b15f3dfbad0e65e881b2efc1c0e1 100644 (file)
@@ -184,7 +184,7 @@ function mountSuspense(
     setActiveBranch(suspense, vnode.ssFallback!)
   } else {
     // Suspense has no async deps. Just resolve.
-    suspense.resolve()
+    suspense.resolve(false, true)
   }
 }
 
@@ -388,7 +388,7 @@ export interface SuspenseBoundary {
   isHydrating: boolean
   isUnmounted: boolean
   effects: Function[]
-  resolve(force?: boolean): void
+  resolve(force?: boolean, sync?: boolean): void
   fallback(fallbackVNode: VNode): void
   move(
     container: RendererElement,
@@ -437,11 +437,10 @@ function createSuspenseBoundary(
 
   // if set `suspensible: true`, set the current suspense as a dep of parent suspense
   let parentSuspenseId: number | undefined
-  const isSuspensible =
-    vnode.props?.suspensible != null && vnode.props.suspensible !== false
+  const isSuspensible = isVNodeSuspensible(vnode)
   if (isSuspensible) {
     if (parentSuspense?.pendingBranch) {
-      parentSuspenseId = parentSuspense?.pendingId
+      parentSuspenseId = parentSuspense.pendingId
       parentSuspense.deps++
     }
   }
@@ -469,7 +468,7 @@ function createSuspenseBoundary(
     isUnmounted: false,
     effects: [],
 
-    resolve(resume = false) {
+    resolve(resume = false, sync = false) {
       if (__DEV__) {
         if (!resume && !suspense.pendingBranch) {
           throw new Error(
@@ -553,7 +552,7 @@ function createSuspenseBoundary(
           parentSuspenseId === parentSuspense.pendingId
         ) {
           parentSuspense.deps--
-          if (parentSuspense.deps === 0) {
+          if (parentSuspense.deps === 0 && !sync) {
             parentSuspense.resolve()
           }
         }
@@ -831,3 +830,7 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
     updateHOCHostEl(parentComponent, el)
   }
 }
+
+function isVNodeSuspensible(vnode: VNode) {
+  return vnode.props?.suspensible != null && vnode.props.suspensible !== false
+}