]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(suspense): make suspense tree-shakeable
authorEvan You <yyx990803@gmail.com>
Tue, 29 Oct 2019 16:30:09 +0000 (12:30 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 29 Oct 2019 16:30:09 +0000 (12:30 -0400)
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/shapeFlags.ts
packages/runtime-core/src/suspense.ts
packages/runtime-core/src/vnode.ts

index cfe9e9a6174b180dcdadcbcf31ed61753872d56b..0bb1327ee46f85b4b03029ed2af2552300797313 100644 (file)
@@ -149,3 +149,13 @@ function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
   }
   return false
 }
+
+export function updateHOCHostEl(
+  { vnode, parent }: ComponentInternalInstance,
+  el: object // HostNode
+) {
+  while (parent && parent.subTree === vnode) {
+    ;(vnode = parent.vnode).el = el
+    parent = parent.parent
+  }
+}
index 009329dcf37317a6d9672c904e639548a37070eb..d9aa3f06f175a9f026163e8fe01d3ec41c042b58 100644 (file)
@@ -6,20 +6,19 @@ import {
   normalizeVNode,
   VNode,
   VNodeChildren,
-  Suspense,
   createVNode
 } from './vnode'
 import {
   ComponentInternalInstance,
   createComponentInstance,
   setupStatefulComponent,
-  handleSetupResult,
   Component,
   Data
 } from './component'
 import {
   renderComponentRoot,
-  shouldUpdateComponent
+  shouldUpdateComponent,
+  updateHOCHostEl
 } from './componentRenderUtils'
 import {
   isString,
@@ -47,51 +46,8 @@ import { pushWarningContext, popWarningContext, warn } from './warning'
 import { invokeDirectiveHook } from './directives'
 import { ComponentPublicInstance } from './componentProxy'
 import { App, createAppAPI } from './apiApp'
-import {
-  SuspenseBoundary,
-  createSuspenseBoundary,
-  normalizeSuspenseChildren
-} from './suspense'
-import { handleError, ErrorCodes, callWithErrorHandling } from './errorHandling'
-
-const prodEffectOptions = {
-  scheduler: queueJob
-}
-
-function createDevEffectOptions(
-  instance: ComponentInternalInstance
-): ReactiveEffectOptions {
-  return {
-    scheduler: queueJob,
-    onTrack: instance.rtc ? e => invokeHooks(instance.rtc!, e) : void 0,
-    onTrigger: instance.rtg ? e => invokeHooks(instance.rtg!, e) : void 0
-  }
-}
-
-function isSameType(n1: VNode, n2: VNode): boolean {
-  return n1.type === n2.type && n1.key === n2.key
-}
-
-function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
-  for (let i = 0; i < hooks.length; i++) {
-    hooks[i](arg)
-  }
-}
-
-export function queuePostRenderEffect(
-  fn: Function | Function[],
-  suspense: SuspenseBoundary<any, any> | null
-) {
-  if (suspense !== null && !suspense.isResolved) {
-    if (isArray(fn)) {
-      suspense.effects.push(...fn)
-    } else {
-      suspense.effects.push(fn)
-    }
-  } else {
-    queuePostFlushCb(fn)
-  }
-}
+import { SuspenseBoundary, SuspenseImpl } from './suspense'
+import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 
 export interface RendererOptions<HostNode = any, HostElement = any> {
   patchProp(
@@ -126,6 +82,75 @@ export type RootRenderFunction<HostNode, HostElement> = (
   dom: HostElement
 ) => void
 
+// An object exposing the internals of a renderer, passed to tree-shakeable
+// features so that they can be decoupled from this file.
+export interface RendererInternals<HostNode = any, HostElement = any> {
+  patch: (
+    n1: VNode<HostNode, HostElement> | null, // null means this is a mount
+    n2: VNode<HostNode, HostElement>,
+    container: HostElement,
+    anchor?: HostNode | null,
+    parentComponent?: ComponentInternalInstance | null,
+    parentSuspense?: SuspenseBoundary<HostNode, HostElement> | null,
+    isSVG?: boolean,
+    optimized?: boolean
+  ) => void
+  unmount: (
+    vnode: VNode<HostNode, HostElement>,
+    parentComponent: ComponentInternalInstance | null,
+    parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
+    doRemove?: boolean
+  ) => void
+  move: (
+    vnode: VNode<HostNode, HostElement>,
+    container: HostElement,
+    anchor: HostNode | null
+  ) => void
+  next: (vnode: VNode<HostNode, HostElement>) => HostNode | null
+  options: RendererOptions<HostNode, HostElement>
+}
+
+const prodEffectOptions = {
+  scheduler: queueJob
+}
+
+function createDevEffectOptions(
+  instance: ComponentInternalInstance
+): ReactiveEffectOptions {
+  return {
+    scheduler: queueJob,
+    onTrack: instance.rtc ? e => invokeHooks(instance.rtc!, e) : void 0,
+    onTrigger: instance.rtg ? e => invokeHooks(instance.rtg!, e) : void 0
+  }
+}
+
+function isSameType(n1: VNode, n2: VNode): boolean {
+  return n1.type === n2.type && n1.key === n2.key
+}
+
+function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
+  for (let i = 0; i < hooks.length; i++) {
+    hooks[i](arg)
+  }
+}
+
+export const queuePostRenderEffect = __FEATURE_SUSPENSE__
+  ? (
+      fn: Function | Function[],
+      suspense: SuspenseBoundary<any, any> | null
+    ) => {
+      if (suspense !== null && !suspense.isResolved) {
+        if (isArray(fn)) {
+          suspense.effects.push(...fn)
+        } else {
+          suspense.effects.push(fn)
+        }
+      } else {
+        queuePostFlushCb(fn)
+      }
+    }
+  : queuePostFlushCb
+
 /**
  * The createRenderer function accepts two generic arguments:
  * HostNode and HostElement, corresponding to Node and Element types in the
@@ -168,6 +193,14 @@ export function createRenderer<
     querySelector: hostQuerySelector
   } = options
 
+  const internals: RendererInternals<HostNode, HostElement> = {
+    patch,
+    unmount,
+    move,
+    next: getNextHostNode,
+    options
+  }
+
   function patch(
     n1: HostVNode | null, // null means this is a mount
     n2: HostVNode,
@@ -217,9 +250,9 @@ export function createRenderer<
           optimized
         )
         break
-      case Suspense:
-        if (__FEATURE_SUSPENSE__) {
-          processSuspense(
+      default:
+        if (shapeFlag & ShapeFlags.ELEMENT) {
+          processElement(
             n1,
             n2,
             container,
@@ -229,13 +262,8 @@ export function createRenderer<
             isSVG,
             optimized
           )
-        } else if (__DEV__) {
-          warn(`Suspense is not enabled in the version of Vue you are using.`)
-        }
-        break
-      default:
-        if (shapeFlag & ShapeFlags.ELEMENT) {
-          processElement(
+        } else if (shapeFlag & ShapeFlags.COMPONENT) {
+          processComponent(
             n1,
             n2,
             container,
@@ -245,8 +273,8 @@ export function createRenderer<
             isSVG,
             optimized
           )
-        } else if (shapeFlag & ShapeFlags.COMPONENT) {
-          processComponent(
+        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
+          ;(type as typeof SuspenseImpl).process(
             n1,
             n2,
             container,
@@ -254,7 +282,8 @@ export function createRenderer<
             parentComponent,
             parentSuspense,
             isSVG,
-            optimized
+            optimized,
+            internals
           )
         } else if (__DEV__) {
           warn('Invalid HostVNode type:', n2.type, `(${typeof n2.type})`)
@@ -725,260 +754,6 @@ export function createRenderer<
     processCommentNode(n1, n2, container, anchor)
   }
 
-  function processSuspense(
-    n1: HostVNode | null,
-    n2: HostVNode,
-    container: HostElement,
-    anchor: HostNode | null,
-    parentComponent: ComponentInternalInstance | null,
-    parentSuspense: HostSuspenseBoundary | null,
-    isSVG: boolean,
-    optimized: boolean
-  ) {
-    if (n1 == null) {
-      mountSuspense(
-        n2,
-        container,
-        anchor,
-        parentComponent,
-        parentSuspense,
-        isSVG,
-        optimized
-      )
-    } else {
-      patchSuspense(
-        n1,
-        n2,
-        container,
-        anchor,
-        parentComponent,
-        isSVG,
-        optimized
-      )
-    }
-  }
-
-  function mountSuspense(
-    n2: HostVNode,
-    container: HostElement,
-    anchor: HostNode | null,
-    parentComponent: ComponentInternalInstance | null,
-    parentSuspense: HostSuspenseBoundary | null,
-    isSVG: boolean,
-    optimized: boolean
-  ) {
-    const hiddenContainer = hostCreateElement('div')
-    const suspense = (n2.suspense = createSuspenseBoundary(
-      n2,
-      parentSuspense,
-      parentComponent,
-      container,
-      hiddenContainer,
-      anchor,
-      isSVG,
-      optimized
-    ))
-
-    const { content, fallback } = normalizeSuspenseChildren(n2)
-    suspense.subTree = content
-    suspense.fallbackTree = fallback
-
-    // start mounting the content subtree in an off-dom container
-    patch(
-      null,
-      content,
-      hiddenContainer,
-      null,
-      parentComponent,
-      suspense,
-      isSVG,
-      optimized
-    )
-    // now check if we have encountered any async deps
-    if (suspense.deps > 0) {
-      // mount the fallback tree
-      patch(
-        null,
-        fallback,
-        container,
-        anchor,
-        parentComponent,
-        null, // fallback tree will not have suspense context
-        isSVG,
-        optimized
-      )
-      n2.el = fallback.el
-    } else {
-      // Suspense has no async deps. Just resolve.
-      resolveSuspense(suspense)
-    }
-  }
-
-  function patchSuspense(
-    n1: HostVNode,
-    n2: HostVNode,
-    container: HostElement,
-    anchor: HostNode | null,
-    parentComponent: ComponentInternalInstance | null,
-    isSVG: boolean,
-    optimized: boolean
-  ) {
-    const suspense = (n2.suspense = n1.suspense)!
-    suspense.vnode = n2
-    const { content, fallback } = normalizeSuspenseChildren(n2)
-    const oldSubTree = suspense.subTree
-    const oldFallbackTree = suspense.fallbackTree
-    if (!suspense.isResolved) {
-      patch(
-        oldSubTree,
-        content,
-        suspense.hiddenContainer,
-        null,
-        parentComponent,
-        suspense,
-        isSVG,
-        optimized
-      )
-      if (suspense.deps > 0) {
-        // still pending. patch the fallback tree.
-        patch(
-          oldFallbackTree,
-          fallback,
-          container,
-          anchor,
-          parentComponent,
-          null, // fallback tree will not have suspense context
-          isSVG,
-          optimized
-        )
-        n2.el = fallback.el
-      }
-      // If deps somehow becomes 0 after the patch it means the patch caused an
-      // async dep component to unmount and removed its dep. It will cause the
-      // suspense to resolve and we don't need to do anything here.
-    } else {
-      // just normal patch inner content as a fragment
-      patch(
-        oldSubTree,
-        content,
-        container,
-        anchor,
-        parentComponent,
-        suspense,
-        isSVG,
-        optimized
-      )
-      n2.el = content.el
-    }
-    suspense.subTree = content
-    suspense.fallbackTree = fallback
-  }
-
-  function resolveSuspense(suspense: HostSuspenseBoundary) {
-    if (__DEV__) {
-      if (suspense.isResolved) {
-        throw new Error(
-          `resolveSuspense() is called on an already resolved suspense boundary.`
-        )
-      }
-      if (suspense.isUnmounted) {
-        throw new Error(
-          `resolveSuspense() is called on an already unmounted suspense boundary.`
-        )
-      }
-    }
-    const {
-      vnode,
-      subTree,
-      fallbackTree,
-      effects,
-      parentComponent,
-      container
-    } = suspense
-
-    // this is initial anchor on mount
-    let { anchor } = suspense
-    // unmount fallback tree
-    if (fallbackTree.el) {
-      // if the fallback tree was mounted, it may have been moved
-      // as part of a parent suspense. get the latest anchor for insertion
-      anchor = getNextHostNode(fallbackTree)
-      unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
-    }
-    // move content from off-dom container to actual container
-    move(subTree as HostVNode, container, anchor)
-    const el = (vnode.el = (subTree as HostVNode).el!)
-    // suspense as the root node of a component...
-    if (parentComponent && parentComponent.subTree === vnode) {
-      parentComponent.vnode.el = el
-      updateHOCHostEl(parentComponent, el)
-    }
-    // check if there is a pending parent suspense
-    let parent = suspense.parent
-    let hasUnresolvedAncestor = false
-    while (parent) {
-      if (!parent.isResolved) {
-        // found a pending parent suspense, merge buffered post jobs
-        // into that parent
-        parent.effects.push(...effects)
-        hasUnresolvedAncestor = true
-        break
-      }
-      parent = parent.parent
-    }
-    // no pending parent suspense, flush all jobs
-    if (!hasUnresolvedAncestor) {
-      queuePostFlushCb(effects)
-    }
-    suspense.isResolved = true
-    // invoke @resolve event
-    const onResolve = vnode.props && vnode.props.onResolve
-    if (isFunction(onResolve)) {
-      onResolve()
-    }
-  }
-
-  function restartSuspense(suspense: HostSuspenseBoundary) {
-    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!)
-    // 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,
@@ -1066,34 +841,10 @@ export function createRenderer<
     if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
       if (!parentSuspense) {
         // TODO handle this properly
-        throw new Error('Async component without a suspense boundary!')
+        throw new Error('Async setup() is used 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) {
-        queueJob(() => {
-          restartSuspense(parentSuspense)
-        })
-      }
-
-      parentSuspense.deps++
-      instance.asyncDep
-        .catch(err => {
-          handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
-        })
-        .then(asyncSetupResult => {
-          // component may be unmounted before resolve
-          if (!instance.isUnmounted && !parentSuspense.isUnmounted) {
-            retryAsyncComponent(
-              instance,
-              asyncSetupResult,
-              parentSuspense,
-              isSVG
-            )
-          }
-        })
+      parentSuspense.registerDep(instance, setupRenderEffect)
 
       // give it a placeholder
       const placeholder = (instance.subTree = createVNode(Comment))
@@ -1116,38 +867,6 @@ export function createRenderer<
     }
   }
 
-  function retryAsyncComponent(
-    instance: ComponentInternalInstance,
-    asyncSetupResult: unknown,
-    parentSuspense: HostSuspenseBoundary,
-    isSVG: boolean
-  ) {
-    parentSuspense.deps--
-    // retry from this component
-    instance.asyncResolved = true
-    const { vnode } = instance
-    if (__DEV__) {
-      pushWarningContext(vnode)
-    }
-    handleSetupResult(instance, asyncSetupResult, parentSuspense)
-    setupRenderEffect(
-      instance,
-      parentSuspense,
-      vnode,
-      // component may have been moved before resolve
-      hostParentNode(instance.subTree.el) as HostElement,
-      getNextHostNode(instance.subTree),
-      isSVG
-    )
-    updateHOCHostEl(instance, vnode.el as HostNode)
-    if (__DEV__) {
-      popWarningContext()
-    }
-    if (parentSuspense.deps === 0) {
-      resolveSuspense(parentSuspense)
-    }
-  }
-
   function setupRenderEffect(
     instance: ComponentInternalInstance,
     parentSuspense: HostSuspenseBoundary | null,
@@ -1237,16 +956,6 @@ export function createRenderer<
     resolveSlots(instance, nextVNode.children)
   }
 
-  function updateHOCHostEl(
-    { vnode, parent }: ComponentInternalInstance,
-    el: HostNode
-  ) {
-    while (parent && parent.subTree === vnode) {
-      ;(vnode = parent.vnode).el = el
-      parent = parent.parent
-    }
-  }
-
   function patchChildren(
     n1: HostVNode | null,
     n2: HostVNode,
@@ -1640,11 +1349,11 @@ export function createRenderer<
     container: HostElement,
     anchor: HostNode | null
   ) {
-    if (vnode.component !== null) {
-      move(vnode.component.subTree, container, anchor)
+    if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
+      move(vnode.component!.subTree, container, anchor)
       return
     }
-    if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
+    if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
       const suspense = vnode.suspense!
       move(
         suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
@@ -1676,8 +1385,6 @@ export function createRenderer<
       props,
       ref,
       type,
-      component,
-      suspense,
       children,
       dynamicChildren,
       shapeFlag,
@@ -1689,13 +1396,13 @@ export function createRenderer<
       setRef(ref, null, parentComponent, null)
     }
 
-    if (component != null) {
-      unmountComponent(component, parentSuspense, doRemove)
+    if (shapeFlag & ShapeFlags.COMPONENT) {
+      unmountComponent(vnode.component!, parentSuspense, doRemove)
       return
     }
 
-    if (__FEATURE_SUSPENSE__ && suspense != null) {
-      unmountSuspense(suspense, parentComponent, parentSuspense, doRemove)
+    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
+      vnode.suspense!.unmount(parentSuspense, doRemove)
       return
     }
 
@@ -1774,24 +1481,11 @@ export function createRenderer<
     ) {
       parentSuspense.deps--
       if (parentSuspense.deps === 0) {
-        resolveSuspense(parentSuspense)
+        parentSuspense.resolve()
       }
     }
   }
 
-  function unmountSuspense(
-    suspense: HostSuspenseBoundary,
-    parentComponent: ComponentInternalInstance | null,
-    parentSuspense: HostSuspenseBoundary | null,
-    doRemove?: boolean
-  ) {
-    suspense.isUnmounted = true
-    unmount(suspense.subTree, parentComponent, parentSuspense, doRemove)
-    if (!suspense.isResolved) {
-      unmount(suspense.fallbackTree, parentComponent, parentSuspense, doRemove)
-    }
-  }
-
   function unmountChildren(
     children: HostVNode[],
     parentComponent: ComponentInternalInstance | null,
@@ -1804,21 +1498,17 @@ export function createRenderer<
     }
   }
 
-  function getNextHostNode({
-    component,
-    suspense,
-    anchor,
-    el
-  }: HostVNode): HostNode | null {
-    if (component !== null) {
-      return getNextHostNode(component.subTree)
+  function getNextHostNode(vnode: HostVNode): HostNode | null {
+    if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
+      return getNextHostNode(vnode.component!.subTree)
     }
-    if (__FEATURE_SUSPENSE__ && suspense !== null) {
+    if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
+      const suspense = vnode.suspense!
       return getNextHostNode(
         suspense.isResolved ? suspense.subTree : suspense.fallbackTree
       )
     }
-    return hostNextSibling((anchor || el)!)
+    return hostNextSibling((vnode.anchor || vnode.el)!)
   }
 
   function setRef(
index 7d00837b40ee04159795b5f20ea8525dabf3b8ba..baa2328b7c4aab3b333dbb66bb499cddf5d2f888 100644 (file)
@@ -7,6 +7,7 @@ export const enum ShapeFlags {
   TEXT_CHILDREN = 1 << 3,
   ARRAY_CHILDREN = 1 << 4,
   SLOTS_CHILDREN = 1 << 5,
+  SUSPENSE = 1 << 6,
   COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
 }
 
@@ -18,5 +19,6 @@ export const PublicShapeFlags = {
   TEXT_CHILDREN: ShapeFlags.TEXT_CHILDREN,
   ARRAY_CHILDREN: ShapeFlags.ARRAY_CHILDREN,
   SLOTS_CHILDREN: ShapeFlags.SLOTS_CHILDREN,
+  SUSPENSE: ShapeFlags.SUSPENSE,
   COMPONENT: ShapeFlags.COMPONENT
 }
index de81f1ab776221a189fe2a35e74a4f9df8c3f7c2..251dcd4177d0bfebb2defe0994b7fc171f4da1f0 100644 (file)
-import { VNode, normalizeVNode, VNodeChild } from './vnode'
-import { ShapeFlags } from '.'
+import { VNode, normalizeVNode, VNodeChild, VNodeTypes } from './vnode'
+import { ShapeFlags } from './shapeFlags'
 import { isFunction } from '@vue/shared'
-import { ComponentInternalInstance } from './component'
+import { ComponentInternalInstance, handleSetupResult } from './component'
 import { Slots } from './componentSlots'
+import { RendererInternals } from './createRenderer'
+import { queuePostFlushCb, queueJob } from './scheduler'
+import { updateHOCHostEl } from './componentRenderUtils'
+import { handleError, ErrorCodes } from './errorHandling'
+import { pushWarningContext, popWarningContext } from './warning'
 
-export const SuspenseSymbol = Symbol(__DEV__ ? 'Suspense key' : undefined)
+export function isSuspenseType(type: VNodeTypes): type is typeof SuspenseImpl {
+  return (type as any).__isSuspenseImpl === true
+}
+
+export const SuspenseImpl = {
+  __isSuspenseImpl: true,
+  process(
+    n1: VNode | null,
+    n2: VNode,
+    container: object,
+    anchor: object | null,
+    parentComponent: ComponentInternalInstance | null,
+    parentSuspense: SuspenseBoundary | null,
+    isSVG: boolean,
+    optimized: boolean,
+    // platform-specific impl passed from renderer
+    rendererInternals: RendererInternals
+  ) {
+    if (n1 == null) {
+      mountSuspense(
+        n2,
+        container,
+        anchor,
+        parentComponent,
+        parentSuspense,
+        isSVG,
+        optimized,
+        rendererInternals
+      )
+    } else {
+      patchSuspense(
+        n1,
+        n2,
+        container,
+        anchor,
+        parentComponent,
+        isSVG,
+        optimized,
+        rendererInternals
+      )
+    }
+  }
+}
+
+function mountSuspense(
+  n2: VNode,
+  container: object,
+  anchor: object | null,
+  parentComponent: ComponentInternalInstance | null,
+  parentSuspense: SuspenseBoundary | null,
+  isSVG: boolean,
+  optimized: boolean,
+  rendererInternals: RendererInternals
+) {
+  const {
+    patch,
+    options: { createElement }
+  } = rendererInternals
+  const hiddenContainer = createElement('div')
+  const suspense = (n2.suspense = createSuspenseBoundary(
+    n2,
+    parentSuspense,
+    parentComponent,
+    container,
+    hiddenContainer,
+    anchor,
+    isSVG,
+    optimized,
+    rendererInternals
+  ))
+
+  const { content, fallback } = normalizeSuspenseChildren(n2)
+  suspense.subTree = content
+  suspense.fallbackTree = fallback
+
+  // start mounting the content subtree in an off-dom container
+  patch(
+    null,
+    content,
+    hiddenContainer,
+    null,
+    parentComponent,
+    suspense,
+    isSVG,
+    optimized
+  )
+  // now check if we have encountered any async deps
+  if (suspense.deps > 0) {
+    // mount the fallback tree
+    patch(
+      null,
+      fallback,
+      container,
+      anchor,
+      parentComponent,
+      null, // fallback tree will not have suspense context
+      isSVG,
+      optimized
+    )
+    n2.el = fallback.el
+  } else {
+    // Suspense has no async deps. Just resolve.
+    suspense.resolve()
+  }
+}
+
+function patchSuspense(
+  n1: VNode,
+  n2: VNode,
+  container: object,
+  anchor: object | null,
+  parentComponent: ComponentInternalInstance | null,
+  isSVG: boolean,
+  optimized: boolean,
+  { patch }: RendererInternals
+) {
+  const suspense = (n2.suspense = n1.suspense)!
+  suspense.vnode = n2
+  const { content, fallback } = normalizeSuspenseChildren(n2)
+  const oldSubTree = suspense.subTree
+  const oldFallbackTree = suspense.fallbackTree
+  if (!suspense.isResolved) {
+    patch(
+      oldSubTree,
+      content,
+      suspense.hiddenContainer,
+      null,
+      parentComponent,
+      suspense,
+      isSVG,
+      optimized
+    )
+    if (suspense.deps > 0) {
+      // still pending. patch the fallback tree.
+      patch(
+        oldFallbackTree,
+        fallback,
+        container,
+        anchor,
+        parentComponent,
+        null, // fallback tree will not have suspense context
+        isSVG,
+        optimized
+      )
+      n2.el = fallback.el
+    }
+    // If deps somehow becomes 0 after the patch it means the patch caused an
+    // async dep component to unmount and removed its dep. It will cause the
+    // suspense to resolve and we don't need to do anything here.
+  } else {
+    // just normal patch inner content as a fragment
+    patch(
+      oldSubTree,
+      content,
+      container,
+      anchor,
+      parentComponent,
+      suspense,
+      isSVG,
+      optimized
+    )
+    n2.el = content.el
+  }
+  suspense.subTree = content
+  suspense.fallbackTree = fallback
+}
 
 export interface SuspenseBoundary<
   HostNode = any,
@@ -25,9 +195,26 @@ export interface SuspenseBoundary<
   isResolved: boolean
   isUnmounted: boolean
   effects: Function[]
+  resolve(): void
+  restart(): void
+  registerDep(
+    instance: ComponentInternalInstance,
+    setupRenderEffect: (
+      instance: ComponentInternalInstance,
+      parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
+      initialVNode: VNode<HostNode, HostElement>,
+      container: HostElement,
+      anchor: HostNode | null,
+      isSVG: boolean
+    ) => void
+  ): void
+  unmount(
+    parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
+    doRemove?: boolean
+  ): void
 }
 
-export function createSuspenseBoundary<HostNode, HostElement>(
+function createSuspenseBoundary<HostNode, HostElement>(
   vnode: VNode<HostNode, HostElement>,
   parent: SuspenseBoundary<HostNode, HostElement> | null,
   parentComponent: ComponentInternalInstance | null,
@@ -35,9 +222,18 @@ export function createSuspenseBoundary<HostNode, HostElement>(
   hiddenContainer: HostElement,
   anchor: HostNode | null,
   isSVG: boolean,
-  optimized: boolean
+  optimized: boolean,
+  rendererInternals: RendererInternals<HostNode, HostElement>
 ): SuspenseBoundary<HostNode, HostElement> {
-  return {
+  const {
+    patch,
+    move,
+    unmount,
+    next,
+    options: { parentNode }
+  } = rendererInternals
+
+  const suspense: SuspenseBoundary<HostNode, HostElement> = {
     vnode,
     parent,
     parentComponent,
@@ -51,11 +247,179 @@ export function createSuspenseBoundary<HostNode, HostElement>(
     fallbackTree: null as any, // will be set immediately after creation
     isResolved: false,
     isUnmounted: false,
-    effects: []
+    effects: [],
+
+    resolve() {
+      if (__DEV__) {
+        if (suspense.isResolved) {
+          throw new Error(
+            `resolveSuspense() is called on an already resolved suspense boundary.`
+          )
+        }
+        if (suspense.isUnmounted) {
+          throw new Error(
+            `resolveSuspense() is called on an already unmounted suspense boundary.`
+          )
+        }
+      }
+      const {
+        vnode,
+        subTree,
+        fallbackTree,
+        effects,
+        parentComponent,
+        container
+      } = suspense
+
+      // this is initial anchor on mount
+      let { anchor } = suspense
+      // unmount fallback tree
+      if (fallbackTree.el) {
+        // 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(fallbackTree)
+        unmount(fallbackTree as VNode, parentComponent, suspense, true)
+      }
+      // move content from off-dom container to actual container
+      move(subTree as VNode, container, anchor)
+      const el = (vnode.el = (subTree as VNode).el!)
+      // suspense as the root node of a component...
+      if (parentComponent && parentComponent.subTree === vnode) {
+        parentComponent.vnode.el = el
+        updateHOCHostEl(parentComponent, el)
+      }
+      // check if there is a pending parent suspense
+      let parent = suspense.parent
+      let hasUnresolvedAncestor = false
+      while (parent) {
+        if (!parent.isResolved) {
+          // found a pending parent suspense, merge buffered post jobs
+          // into that parent
+          parent.effects.push(...effects)
+          hasUnresolvedAncestor = true
+          break
+        }
+        parent = parent.parent
+      }
+      // no pending parent suspense, flush all jobs
+      if (!hasUnresolvedAncestor) {
+        queuePostFlushCb(effects)
+      }
+      suspense.isResolved = true
+      // invoke @resolve event
+      const onResolve = vnode.props && vnode.props.onResolve
+      if (isFunction(onResolve)) {
+        onResolve()
+      }
+    },
+
+    restart() {
+      suspense.isResolved = false
+      const {
+        vnode,
+        subTree,
+        fallbackTree,
+        parentComponent,
+        container,
+        hiddenContainer,
+        isSVG,
+        optimized
+      } = suspense
+
+      // move content tree back to the off-dom container
+      const anchor = next(subTree)
+      move(subTree as VNode, 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 VNode).el!)
+      // 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()
+      }
+    },
+
+    registerDep(instance, setupRenderEffect) {
+      // suspense is already resolved, need to recede.
+      // use queueJob so it's handled synchronously after patching the current
+      // suspense tree
+      if (suspense.isResolved) {
+        queueJob(() => {
+          suspense.restart()
+        })
+      }
+
+      suspense.deps++
+      instance
+        .asyncDep!.catch(err => {
+          handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
+        })
+        .then(asyncSetupResult => {
+          // retry when the setup() promise resolves.
+          // component may have been unmounted before resolve.
+          if (instance.isUnmounted || suspense.isUnmounted) {
+            return
+          }
+          suspense.deps--
+          // retry from this component
+          instance.asyncResolved = true
+          const { vnode } = instance
+          if (__DEV__) {
+            pushWarningContext(vnode)
+          }
+          handleSetupResult(instance, asyncSetupResult, suspense)
+          setupRenderEffect(
+            instance,
+            suspense,
+            vnode,
+            // component may have been moved before resolve
+            parentNode(instance.subTree.el)!,
+            next(instance.subTree),
+            isSVG
+          )
+          updateHOCHostEl(instance, vnode.el)
+          if (__DEV__) {
+            popWarningContext()
+          }
+          if (suspense.deps === 0) {
+            suspense.resolve()
+          }
+        })
+    },
+
+    unmount(parentSuspense, doRemove) {
+      suspense.isUnmounted = true
+      unmount(suspense.subTree, parentComponent, parentSuspense, doRemove)
+      if (!suspense.isResolved) {
+        unmount(
+          suspense.fallbackTree,
+          parentComponent,
+          parentSuspense,
+          doRemove
+        )
+      }
+    }
   }
+
+  return suspense
 }
 
-export function normalizeSuspenseChildren(
+function normalizeSuspenseChildren(
   vnode: VNode
 ): {
   content: VNode
index 25e80821f47d7be0fa48911f8ba8c92d24954244..cf8c4e5eb2d281bb466d59426efb458e42f31fde 100644 (file)
@@ -16,15 +16,18 @@ import { RawSlots } from './componentSlots'
 import { ShapeFlags } from './shapeFlags'
 import { isReactive } from '@vue/reactivity'
 import { AppContext } from './apiApp'
-import { SuspenseBoundary } from './suspense'
+import { SuspenseBoundary, isSuspenseType } from './suspense'
 import { DirectiveBinding } from './directives'
+import { SuspenseImpl } from './suspense'
 
 export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined)
 export const Portal = Symbol(__DEV__ ? 'Portal' : undefined)
-export const Suspense = Symbol(__DEV__ ? 'Suspense' : undefined)
 export const Text = Symbol(__DEV__ ? 'Text' : undefined)
 export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
 
+const Suspense = __FEATURE_SUSPENSE__ ? SuspenseImpl : null
+export { Suspense }
+
 export type VNodeTypes =
   | string
   | Component
@@ -32,7 +35,7 @@ export type VNodeTypes =
   | typeof Portal
   | typeof Text
   | typeof Comment
-  | typeof Suspense
+  | typeof SuspenseImpl
 
 type VNodeChildAtom<HostNode, HostElement> =
   | VNode<HostNode, HostElement>
@@ -187,11 +190,13 @@ export function createVNode(
   // encode the vnode type information into a bitmap
   const shapeFlag = isString(type)
     ? ShapeFlags.ELEMENT
-    : isObject(type)
-      ? ShapeFlags.STATEFUL_COMPONENT
-      : isFunction(type)
-        ? ShapeFlags.FUNCTIONAL_COMPONENT
-        : 0
+    : __FEATURE_SUSPENSE__ && isSuspenseType(type)
+      ? ShapeFlags.SUSPENSE
+      : isObject(type)
+        ? ShapeFlags.STATEFUL_COMPONENT
+        : isFunction(type)
+          ? ShapeFlags.FUNCTIONAL_COMPONENT
+          : 0
 
   const vnode: VNode = {
     _isVNode: true,