From: Evan You Date: Mon, 9 Sep 2019 20:00:50 +0000 (-0400) Subject: wip: somewhat working suspense X-Git-Tag: v3.0.0-alpha.0~800 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=02bb156314acc01318af656ad440bba678fc2c17;p=thirdparty%2Fvuejs%2Fcore.git wip: somewhat working suspense --- diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 59dc958b43..e4524d33be 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -78,6 +78,10 @@ export interface ComponentInternalInstance { components: Record directives: Record + asyncDep: Promise | 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 + 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 diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index dcd0fc5696..d76d400d60 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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, + 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( diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 365b23f1bc..aebfb64020 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts index 65e2184fc4..b6027bbb69 100644 --- a/packages/runtime-core/src/suspense.ts +++ b/packages/runtime-core/src/suspense.ts @@ -1,46 +1,54 @@ import { VNode } from './vnode' -import { queuePostFlushCb } from './scheduler' export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol() -export interface SuspenseBoundary { +export interface SuspenseBoundary< + HostNode, + HostElement, + HostVNode = VNode +> { + vnode: HostVNode parent: SuspenseBoundary | null - contentTree: VNode | null - fallbackTree: VNode | 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( - parent: SuspenseBoundary | null, - container: HostElement + vnode: VNode, + parent: SuspenseBoundary | null ): SuspenseBoundary { + let retry: Function + let resolve: Function const suspense: SuspenseBoundary = { + 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() } } diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index e7b3e86b7a..bea0a687be 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -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,