diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index cfe9e9a61..0bb1327ee 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -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 + } +} diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 009329dcf..d9aa3f06f 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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 | 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 { patchProp( @@ -126,6 +82,75 @@ export type RootRenderFunction = ( 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 { + patch: ( + n1: VNode | null, // null means this is a mount + n2: VNode, + container: HostElement, + anchor?: HostNode | null, + parentComponent?: ComponentInternalInstance | null, + parentSuspense?: SuspenseBoundary | null, + isSVG?: boolean, + optimized?: boolean + ) => void + unmount: ( + vnode: VNode, + parentComponent: ComponentInternalInstance | null, + parentSuspense: SuspenseBoundary | null, + doRemove?: boolean + ) => void + move: ( + vnode: VNode, + container: HostElement, + anchor: HostNode | null + ) => void + next: (vnode: VNode) => HostNode | null + options: RendererOptions +} + +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 | 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 = { + patch, + unmount, + move, + next: getNextHostNode, + options + } + function patch( n1: HostVNode | null, // null means this is a mount n2: HostVNode, @@ -217,22 +250,6 @@ export function createRenderer< optimized ) break - case Suspense: - if (__FEATURE_SUSPENSE__) { - processSuspense( - n1, - n2, - container, - anchor, - parentComponent, - parentSuspense, - 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( @@ -256,6 +273,18 @@ export function createRenderer< isSVG, optimized ) + } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { + ;(type as typeof SuspenseImpl).process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + isSVG, + 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( diff --git a/packages/runtime-core/src/shapeFlags.ts b/packages/runtime-core/src/shapeFlags.ts index 7d00837b4..baa2328b7 100644 --- a/packages/runtime-core/src/shapeFlags.ts +++ b/packages/runtime-core/src/shapeFlags.ts @@ -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 } diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts index de81f1ab7..251dcd417 100644 --- a/packages/runtime-core/src/suspense.ts +++ b/packages/runtime-core/src/suspense.ts @@ -1,10 +1,180 @@ -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 | null, + initialVNode: VNode, + container: HostElement, + anchor: HostNode | null, + isSVG: boolean + ) => void + ): void + unmount( + parentSuspense: SuspenseBoundary | null, + doRemove?: boolean + ): void } -export function createSuspenseBoundary( +function createSuspenseBoundary( vnode: VNode, parent: SuspenseBoundary | null, parentComponent: ComponentInternalInstance | null, @@ -35,9 +222,18 @@ export function createSuspenseBoundary( hiddenContainer: HostElement, anchor: HostNode | null, isSVG: boolean, - optimized: boolean + optimized: boolean, + rendererInternals: RendererInternals ): SuspenseBoundary { - return { + const { + patch, + move, + unmount, + next, + options: { parentNode } + } = rendererInternals + + const suspense: SuspenseBoundary = { vnode, parent, parentComponent, @@ -51,11 +247,179 @@ export function createSuspenseBoundary( 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 diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 25e80821f..cf8c4e5eb 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 = | VNode @@ -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,