]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: somewhat working suspense
authorEvan You <yyx990803@gmail.com>
Mon, 9 Sep 2019 20:00:50 +0000 (16:00 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 11 Sep 2019 15:10:13 +0000 (11:10 -0400)
packages/runtime-core/src/component.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/suspense.ts
packages/runtime-dom/src/nodeOps.ts

index 59dc958b43e4e24e08afe1a7a3fbbfe80beaca10..e4524d33be2fbd1e5fc1998d3593582741de073f 100644 (file)
@@ -78,6 +78,10 @@ export interface ComponentInternalInstance {
   components: Record<string, Component>
   directives: Record<string, Directive>
 
+  asyncDep: Promise<any> | null
+  asyncResult: any
+  asyncResolved: boolean
+
   // the rest are only for stateful components
   renderContext: Data
   data: Data
@@ -146,6 +150,11 @@ export function createComponentInstance(
     components: Object.create(appContext.components),
     directives: Object.create(appContext.directives),
 
+    // async dependency management
+    asyncDep: null,
+    asyncResult: null,
+    asyncResolved: false,
+
     // user namespace for storing whatever the user assigns to `this`
     user: {},
 
@@ -206,7 +215,6 @@ export const setCurrentInstance = (
 }
 
 export function setupStatefulComponent(instance: ComponentInternalInstance) {
-  currentInstance = instance
   const Component = instance.type as ComponentOptions
   // 1. create render proxy
   instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
@@ -219,62 +227,76 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
   if (setup) {
     const setupContext = (instance.setupContext =
       setup.length > 1 ? createSetupContext(instance) : null)
+
+    currentInstance = instance
     const setupResult = callWithErrorHandling(
       setup,
       instance,
       ErrorCodes.SETUP_FUNCTION,
       [propsProxy, setupContext]
     )
+    currentInstance = null
 
-    if (isFunction(setupResult)) {
-      // setup returned an inline render function
-      instance.render = setupResult
+    if (
+      setupResult &&
+      isFunction(setupResult.then) &&
+      isFunction(setupResult.catch)
+    ) {
+      // async setup returned Promise.
+      // bail here and wait for re-entry.
+      instance.asyncDep = setupResult as Promise<any>
+      return
     } else {
-      if (__DEV__) {
-        if (!Component.render) {
-          warn(
-            `Component is missing render function. Either provide a template or ` +
-              `return a render function from setup().`
-          )
-        }
-        if (
-          setupResult &&
-          typeof setupResult.then === 'function' &&
-          typeof setupResult.catch === 'function'
-        ) {
-          warn(`setup() returned a Promise. setup() cannot be async.`)
-        }
-      }
-      // setup returned bindings.
-      // assuming a render function compiled from template is present.
-      if (isObject(setupResult)) {
-        instance.renderContext = reactive(setupResult)
-      } else if (__DEV__ && setupResult !== undefined) {
-        warn(
-          `setup() should return an object. Received: ${
-            setupResult === null ? 'null' : typeof setupResult
-          }`
-        )
-      }
-      instance.render = (Component.render || NOOP) as RenderFunction
+      handleSetupResult(instance, setupResult)
     }
   } else {
+    finishComponentSetup(instance)
+  }
+}
+
+export function handleSetupResult(
+  instance: ComponentInternalInstance,
+  setupResult: unknown
+) {
+  if (isFunction(setupResult)) {
+    // setup returned an inline render function
+    instance.render = setupResult as RenderFunction
+  } else if (isObject(setupResult)) {
+    // setup returned bindings.
+    // assuming a render function compiled from template is present.
+    instance.renderContext = reactive(setupResult)
+  } else if (__DEV__ && setupResult !== undefined) {
+    warn(
+      `setup() should return an object. Received: ${
+        setupResult === null ? 'null' : typeof setupResult
+      }`
+    )
+  }
+  finishComponentSetup(instance)
+}
+
+function finishComponentSetup(instance: ComponentInternalInstance) {
+  const Component = instance.type as ComponentOptions
+  if (!instance.render) {
     if (__DEV__ && !Component.render) {
       warn(
         `Component is missing render function. Either provide a template or ` +
           `return a render function from setup().`
       )
     }
-    instance.render = Component.render as RenderFunction
+    instance.render = (Component.render || NOOP) as RenderFunction
   }
+
   // support for 2.x options
   if (__FEATURE_OPTIONS__) {
+    currentInstance = instance
     applyOptions(instance, Component)
+    currentInstance = null
   }
+
   if (instance.renderContext === EMPTY_OBJ) {
     instance.renderContext = reactive({})
   }
-  currentInstance = null
 }
 
 // used to identify a setup context proxy
index dcd0fc5696fc0ec26443de12ee9c4e718ede65c2..d76d400d60e976067573727b93a385f953e656f9 100644 (file)
@@ -12,7 +12,9 @@ import {
 import {
   ComponentInternalInstance,
   createComponentInstance,
-  setupStatefulComponent
+  setupStatefulComponent,
+  handleSetupResult,
+  setCurrentInstance
 } from './component'
 import {
   renderComponentRoot,
@@ -43,6 +45,7 @@ import { invokeDirectiveHook } from './directives'
 import { ComponentPublicInstance } from './componentPublicInstanceProxy'
 import { App, createAppAPI } from './apiApp'
 import { SuspenseBoundary, createSuspenseBoundary } from './suspense'
+import { provide } from './apiInject'
 
 const prodEffectOptions = {
   scheduler: queueJob
@@ -604,16 +607,68 @@ export function createRenderer<
     if (n1 == null) {
       const contentContainer = hostCreateElement('div')
       const suspense = (n2.suspense = createSuspenseBoundary(
-        parentSuspense,
-        contentContainer
+        n2,
+        parentSuspense
       ))
 
+      suspense.onRetry(() => {
+        processFragment(
+          suspense.oldContentTree,
+          suspense.contentTree as HostVNode,
+          contentContainer,
+          null,
+          parentComponent,
+          isSVG,
+          optimized
+        )
+        if (suspense.deps > 0) {
+          // still pending.
+          // patch the fallback tree.
+        } else {
+          suspense.resolve()
+        }
+      })
+
+      suspense.onResolve(() => {
+        // move content from off-dom container to actual container
+        ;(suspense.contentTree as any).children.forEach((vnode: HostVNode) => {
+          move(vnode, container, anchor)
+        })
+        suspense.vnode.el = (suspense.contentTree as HostVNode).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.bufferedJobs.push(...suspense.bufferedJobs)
+            hasUnresolvedAncestor = true
+            break
+          }
+        }
+        // no pending parent suspense, flush all jobs
+        if (!hasUnresolvedAncestor) {
+          queuePostFlushCb(suspense.bufferedJobs)
+        }
+        suspense.isResolved = true
+      })
+
+      // TODO pass it down as an arg instead
+      if (parentComponent) {
+        setCurrentInstance(parentComponent)
+        provide('suspense', suspense)
+        setCurrentInstance(null)
+      }
+
       // start mounting the subtree off-dom
-      // - TODO tracking async deps and buffering postQueue jobs on current boundary
-      const contentTree = (suspense.contentTree = childrenToFragment(n2))
+      // TODO should buffer postQueue jobs on current boundary
+      const contentTree = (suspense.contentTree = suspense.oldContentTree = childrenToFragment(
+        n2
+      ))
       processFragment(
         null,
-        contentTree as VNode<HostNode, HostElement>,
+        contentTree as HostVNode,
         contentContainer,
         null,
         parentComponent,
@@ -625,6 +680,7 @@ export function createRenderer<
         // yes: mount the fallback tree.
         // Each time an async dep resolves, it pings the boundary
         // and causes a re-entry.
+        console.log('fallback')
       } else {
         suspense.resolve()
       }
@@ -633,23 +689,23 @@ export function createRenderer<
         HostNode,
         HostElement
       >
-      const oldContentTree = suspense.contentTree
+      suspense.vnode = n2
+      const oldContentTree = (suspense.oldContentTree = suspense.contentTree)
       const newContentTree = (suspense.contentTree = childrenToFragment(n2))
-      // patch suspense subTree as fragment
-      processFragment(
-        oldContentTree,
-        newContentTree,
-        container,
-        anchor,
-        parentComponent,
-        isSVG,
-        optimized
-      )
-      if (suspense.deps > 0) {
-        // still pending.
-        // patch the fallback tree.
+      if (!suspense.isResolved) {
+        suspense.retry()
       } else {
-        suspense.resolve()
+        // just normal patch inner content as a fragment
+        processFragment(
+          oldContentTree,
+          newContentTree,
+          container,
+          null,
+          parentComponent,
+          isSVG,
+          optimized
+        )
+        n2.el = newContentTree.el
       }
     }
   }
@@ -676,10 +732,24 @@ export function createRenderer<
     } else {
       const instance = (n2.component =
         n1.component) as ComponentInternalInstance
-      if (shouldUpdateComponent(n1, n2, optimized)) {
+      // async still pending
+      if (instance.asyncDep && !instance.asyncResolved) {
+        return
+      }
+      // a resolved async component, on successful re-entry.
+      // pickup the mounting process and setup render effect
+      if (!instance.update) {
+        setupRenderEffect(instance, n2, container, anchor, isSVG)
+      } else if (
+        shouldUpdateComponent(n1, n2, optimized) ||
+        (instance.provides.suspense &&
+          !(instance.provides.suspense as any).isResolved)
+      ) {
+        // normal update
         instance.next = n2
         instance.update()
       } else {
+        // no update needed. just copy over properties
         n2.component = n1.component
         n2.el = n1.el
       }
@@ -720,6 +790,37 @@ export function createRenderer<
       setupStatefulComponent(instance)
     }
 
+    // setup() is async. This component relies on async logic to be resolved
+    // before proceeding
+    if (instance.asyncDep) {
+      const suspense = (instance as any).provides.suspense
+      if (!suspense) {
+        throw new Error('Async component without a suspense boundary!')
+      }
+      suspense.deps++
+      instance.asyncDep.then(res => {
+        instance.asyncResolved = true
+        handleSetupResult(instance, res)
+        suspense.deps--
+        suspense.retry()
+      })
+      return
+    }
+
+    setupRenderEffect(instance, initialVNode, container, anchor, isSVG)
+
+    if (__DEV__) {
+      popWarningContext()
+    }
+  }
+
+  function setupRenderEffect(
+    instance: ComponentInternalInstance,
+    initialVNode: HostVNode,
+    container: HostElement,
+    anchor: HostNode | null,
+    isSVG: boolean
+  ) {
     // create reactive effect for rendering
     let mounted = false
     instance.update = effect(function componentEffect() {
@@ -751,7 +852,7 @@ export function createRenderer<
           next.component = instance
           instance.vnode = next
           instance.next = null
-          resolveProps(instance, next.props, propsOptions)
+          resolveProps(instance, next.props, (initialVNode.type as any).props)
           resolveSlots(instance, next.children)
         }
         const prevTree = instance.subTree
@@ -797,10 +898,6 @@ export function createRenderer<
         }
       }
     }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
-
-    if (__DEV__) {
-      popWarningContext()
-    }
   }
 
   function patchChildren(
index 365b23f1bc58353a194851bc4ed97bd61bec33bd..aebfb64020ebfc3efe315373e293529086dfbeec 100644 (file)
@@ -19,7 +19,7 @@ export {
   createBlock
 } from './vnode'
 // VNode type symbols
-export { Text, Empty, Fragment, Portal } from './vnode'
+export { Text, Empty, Fragment, Portal, Suspense } from './vnode'
 // VNode flags
 export { PublicPatchFlags as PatchFlags } from './patchFlags'
 export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
index 65e2184fc4a185d9629cd058a3d4c61d84befddc..b6027bbb69210b47730b4057b8caea9c5e86c96f 100644 (file)
@@ -1,46 +1,54 @@
 import { VNode } from './vnode'
-import { queuePostFlushCb } from './scheduler'
 
 export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
 
-export interface SuspenseBoundary<HostNode, HostElement> {
+export interface SuspenseBoundary<
+  HostNode,
+  HostElement,
+  HostVNode = VNode<HostNode, HostElement>
+> {
+  vnode: HostVNode
   parent: SuspenseBoundary<HostNode, HostElement> | null
-  contentTree: VNode<HostNode, HostElement> | null
-  fallbackTree: VNode<HostNode, HostElement> | null
+  contentTree: HostVNode | null
+  oldContentTree: HostVNode | null
+  fallbackTree: HostVNode | null
+  oldFallbackTree: HostVNode | null
   deps: number
   isResolved: boolean
   bufferedJobs: Function[]
-  container: HostElement
+  onRetry(fn: Function): void
+  retry(): void
+  onResolve(fn: Function): void
   resolve(): void
 }
 
 export function createSuspenseBoundary<HostNode, HostElement>(
-  parent: SuspenseBoundary<HostNode, HostElement> | null,
-  container: HostElement
+  vnode: VNode<HostNode, HostElement>,
+  parent: SuspenseBoundary<HostNode, HostElement> | null
 ): SuspenseBoundary<HostNode, HostElement> {
+  let retry: Function
+  let resolve: Function
   const suspense: SuspenseBoundary<HostNode, HostElement> = {
+    vnode,
     parent,
-    container,
     deps: 0,
     contentTree: null,
+    oldContentTree: null,
     fallbackTree: null,
+    oldFallbackTree: null,
     isResolved: false,
     bufferedJobs: [],
+    onRetry(fn: Function) {
+      retry = fn
+    },
+    retry() {
+      retry()
+    },
+    onResolve(fn: Function) {
+      resolve = fn
+    },
     resolve() {
-      suspense.isResolved = true
-      let parent = suspense.parent
-      let hasUnresolvedAncestor = false
-      while (parent) {
-        if (!parent.isResolved) {
-          parent.bufferedJobs.push(...suspense.bufferedJobs)
-          hasUnresolvedAncestor = true
-          break
-        }
-      }
-      if (!hasUnresolvedAncestor) {
-        queuePostFlushCb(suspense.bufferedJobs)
-      }
-      suspense.isResolved = true
+      resolve()
     }
   }
 
index e7b3e86b7a1ec28bf227945afdfd7033f7c44a97..bea0a687beba397f09f1d3217d2754cd5a169549 100644 (file)
@@ -32,7 +32,12 @@ export const nodeOps = {
     el.textContent = text
   },
 
-  parentNode: (node: Node): Node | null => node.parentNode,
+  parentNode: (node: Node): Node | null => {
+    if (!node) {
+      debugger
+    }
+    return node.parentNode
+  },
 
   nextSibling: (node: Node): Node | null => node.nextSibling,