From 8b3aa60a18da309d687ca7dbdaec953729b25887 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 7 Sep 2019 11:28:40 -0400 Subject: [PATCH] wip: suspense ideas --- packages/runtime-core/src/createRenderer.ts | 61 ++++++++++++++++++++- packages/runtime-core/src/suspense.ts | 48 ++++++++++++++++ packages/runtime-core/src/vnode.ts | 6 ++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/runtime-core/src/suspense.ts diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index d20466f1..33cae409 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -5,12 +5,14 @@ import { Portal, normalizeVNode, VNode, - VNodeChildren + VNodeChildren, + Suspense } from './vnode' import { ComponentInternalInstance, createComponentInstance, - setupStatefulComponent + setupStatefulComponent, + setCurrentInstance } from './component' import { renderComponentRoot, @@ -40,6 +42,12 @@ import { pushWarningContext, popWarningContext, warn } from './warning' import { invokeDirectiveHook } from './directives' import { ComponentPublicInstance } from './componentPublicInstanceProxy' import { App, createAppAPI } from './apiApp' +import { + SuspenseSymbol, + createSuspenseBoundary, + SuspenseBoundary +} from './suspense' +import { provide } from './apiInject' const prodEffectOptions = { scheduler: queueJob @@ -187,6 +195,17 @@ export function createRenderer< optimized ) break + case Suspense: + processSuspense( + n1, + n2, + container, + anchor, + parentComponent, + isSVG, + optimized + ) + break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( @@ -575,6 +594,44 @@ export function createRenderer< processEmptyNode(n1, n2, container, anchor) } + function processSuspense( + n1: HostVNode | null, + n2: HostVNode, + container: HostElement, + anchor: HostNode | null, + parentComponent: ComponentInternalInstance | null, + isSVG: boolean, + optimized: boolean + ) { + if (n1 == null) { + const parentSuspense = + parentComponent && + (parentComponent.provides[SuspenseSymbol as any] as SuspenseBoundary) + const suspense = (n2.suspense = createSuspenseBoundary(parentSuspense)) + + // provide this as the parent suspense for descendents + setCurrentInstance(parentComponent) + provide(SuspenseSymbol, suspense) + setCurrentInstance(null) + + // start mounting the subtree off-dom + // - tracking async deps and buffering postQueue jobs on current boundary + + // now check if we have encountered any async deps + // yes: mount the fallback tree. + // Each time an async dep resolves, it pings the boundary + // and causes a re-entry. + + // no: just mount the tree + // - if have parent boundary that is still not resolved: + // merge the buffered jobs into parent + // - else: flush buffered jobs. + // - mark resolved. + } else { + const suspense = (n2.suspense = n1.suspense) as SuspenseBoundary + } + } + function processComponent( n1: HostVNode | null, n2: HostVNode, diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts new file mode 100644 index 00000000..8e8bdebf --- /dev/null +++ b/packages/runtime-core/src/suspense.ts @@ -0,0 +1,48 @@ +import { warn } from './warning' + +export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol() + +export interface SuspenseBoundary { + deps: number + isResolved: boolean + parent: SuspenseBoundary | null + ping(): void + resolve(): void + onResolve(cb: () => void): void +} + +export function createSuspenseBoundary( + parent: SuspenseBoundary | null +): SuspenseBoundary { + let onResolve: () => void + + if (parent && !parent.isResolved) { + parent.deps++ + } + + const boundary: SuspenseBoundary = { + deps: 0, + isResolved: false, + parent: parent && parent.isResolved ? parent : null, + ping() { + // one of the deps resolved - re-entry from root suspense + if (boundary.parent) { + } + if (__DEV__ && boundary.deps < 0) { + warn(`Suspense boundary pinged when deps === 0. This is a bug.`) + } + }, + resolve() { + boundary.isResolved = true + if (parent && !parent.isResolved) { + parent.ping() + } else { + onResolve && onResolve() + } + }, + onResolve(cb: () => void) { + onResolve = cb + } + } + return boundary +} diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 1df7641a..03f4aef4 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -12,11 +12,13 @@ import { PatchFlags } from './patchFlags' import { ShapeFlags } from './shapeFlags' import { isReactive } from '@vue/reactivity' import { AppContext } from './apiApp' +import { SuspenseBoundary } from './suspense' export const Fragment = __DEV__ ? Symbol('Fragment') : Symbol() export const Text = __DEV__ ? Symbol('Text') : Symbol() export const Empty = __DEV__ ? Symbol('Empty') : Symbol() export const Portal = __DEV__ ? Symbol('Portal') : Symbol() +export const Suspense = __DEV__ ? Symbol('Suspense') : Symbol() export type VNodeTypes = | string @@ -26,6 +28,7 @@ export type VNodeTypes = | typeof Portal | typeof Text | typeof Empty + | typeof Suspense type VNodeChildAtom = | VNode @@ -58,6 +61,7 @@ export interface VNode { ref: string | Function | null children: NormalizedChildren component: ComponentInternalInstance | null + suspense: SuspenseBoundary | null // DOM el: HostNode | null @@ -168,6 +172,7 @@ export function createVNode( ref: (props && props.ref) || null, children: null, component: null, + suspense: null, el: null, anchor: null, target: null, @@ -221,6 +226,7 @@ export function cloneVNode(vnode: VNode): VNode { // mounted VNodes. If they are somehow not null, this means we have // encountered an already-mounted vnode being used again. component: null, + suspense: null, el: null, anchor: null }