]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: re-suspense when encountering new async deps in resolved state
authorEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 16:16:01 +0000 (12:16 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 16:16:01 +0000 (12:16 -0400)
packages/runtime-core/__tests__/rendererSuspense.spec.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/suspense.ts

index e73a81c32c4d440c0e4f0c65173029f546d8d5e4..7068ab0aa228ec12da17affeda520557b255cad1 100644 (file)
@@ -658,7 +658,49 @@ describe('renderer: suspense', () => {
     )
   })
 
-  test.todo('new async dep after resolve should cause suspense to restart')
+  test('new async dep after resolve should cause suspense to restart', async () => {
+    const toggle = ref(false)
+
+    const ChildA = createAsyncComponent({
+      setup() {
+        return () => h('div', 'Child A')
+      }
+    })
+
+    const ChildB = createAsyncComponent({
+      setup() {
+        return () => h('div', 'Child B')
+      }
+    })
+
+    const Comp = {
+      setup() {
+        return () =>
+          h(Suspense, null, {
+            default: [h(ChildA), toggle.value ? h(ChildB) : null],
+            fallback: h('div', 'root fallback')
+          })
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(Comp), root)
+    expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
+
+    await deps[0]
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`)
+
+    toggle.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
+
+    await deps[1]
+    await nextTick()
+    expect(serializeInner(root)).toBe(
+      `<!----><div>Child A</div><div>Child B</div><!---->`
+    )
+  })
 
   test.todo('portal inside suspense')
 })
index 73488df337696c667b081f0a84361922de253b69..22cf5ef90d66cd9fb84c77fec7a886840c759523 100644 (file)
@@ -729,7 +729,9 @@ export function createRenderer<
       parentComponent,
       container,
       hiddenContainer,
-      anchor
+      anchor,
+      isSVG,
+      optimized
     ))
 
     const { content, fallback } = normalizeSuspenseChildren(n2)
@@ -831,20 +833,20 @@ export function createRenderer<
     if (__DEV__) {
       if (suspense.isResolved) {
         throw new Error(
-          `suspense.resolve() is called when it's already resolved`
+          `resolveSuspense() is called on an already resolved suspense boundary.`
         )
       }
       if (suspense.isUnmounted) {
         throw new Error(
-          `suspense.resolve() is called when it's already unmounted`
+          `resolveSuspense() is called on an already unmounted suspense boundary.`
         )
       }
     }
     const {
+      vnode,
       subTree,
       fallbackTree,
       effects,
-      vnode,
       parentComponent,
       container
     } = suspense
@@ -891,6 +893,47 @@ export function createRenderer<
     }
   }
 
+  function restartSuspense(suspense: HostSuspsenseBoundary) {
+    suspense.isResolved = false
+    const {
+      vnode,
+      subTree,
+      fallbackTree,
+      parentComponent,
+      container,
+      hiddenContainer,
+      isSVG,
+      optimized
+    } = suspense
+
+    // move content tree back to the off-dom container
+    const anchor = getNextHostNode(subTree)
+    move(subTree as HostVNode, hiddenContainer, null)
+    // remount the fallback tree
+    patch(
+      null,
+      fallbackTree,
+      container,
+      anchor,
+      parentComponent,
+      null, // fallback tree will not have suspense context
+      isSVG,
+      optimized
+    )
+    const el = (vnode.el = (fallbackTree as HostVNode).el as HostNode)
+    // suspense as the root node of a component...
+    if (parentComponent && parentComponent.subTree === vnode) {
+      parentComponent.vnode.el = el
+      updateHOCHostEl(parentComponent, el)
+    }
+
+    // invoke @suspense event
+    const onSuspense = vnode.props && vnode.props.onSuspense
+    if (isFunction(onSuspense)) {
+      onSuspense()
+    }
+  }
+
   function processComponent(
     n1: HostVNode | null,
     n2: HostVNode,
@@ -986,10 +1029,16 @@ export function createRenderer<
         // TODO handle this properly
         throw new Error('Async component without a suspense boundary!')
       }
+
+      // parent suspense already resolved, need to re-suspense
+      // use queueJob so it's handled synchronously after patching the current
+      // suspense tree
       if (parentSuspense.isResolved) {
-        // TODO if parentSuspense is already resolved it needs to enter waiting
-        // state again
+        queueJob(() => {
+          restartSuspense(parentSuspense)
+        })
       }
+
       parentSuspense.deps++
       instance.asyncDep
         .catch(err => {
@@ -1006,6 +1055,7 @@ export function createRenderer<
             )
           }
         })
+
       // give it a placeholder
       const placeholder = (instance.subTree = createVNode(Empty))
       processEmptyNode(null, placeholder, container, anchor)
@@ -1118,8 +1168,7 @@ export function createRenderer<
           parentSuspense,
           isSVG
         )
-        let current = instance.vnode
-        current.el = nextTree.el
+        instance.vnode.el = nextTree.el
         if (next === null) {
           // self-triggered update. In case of HOC, update parent component
           // vnode el. HOC is indicated by parent instance's subTree pointing
index fb8c36fbeecf9ede2c252d8f1761ba743a83b4e9..09a067ab017f068c1c0c8bab57cf8adda3a6102c 100644 (file)
@@ -13,6 +13,8 @@ export interface SuspenseBoundary<
   vnode: HostVNode
   parent: SuspenseBoundary<HostNode, HostElement> | null
   parentComponent: ComponentInternalInstance | null
+  isSVG: boolean
+  optimized: boolean
   container: HostElement
   hiddenContainer: HostElement
   anchor: HostNode | null
@@ -30,12 +32,16 @@ export function createSuspenseBoundary<HostNode, HostElement>(
   parentComponent: ComponentInternalInstance | null,
   container: HostElement,
   hiddenContainer: HostElement,
-  anchor: HostNode | null
+  anchor: HostNode | null,
+  isSVG: boolean,
+  optimized: boolean
 ): SuspenseBoundary<HostNode, HostElement> {
   return {
     vnode,
     parent,
     parentComponent,
+    isSVG,
+    optimized,
     container,
     hiddenContainer,
     anchor,