refactor(suspense): make suspense tree-shakeable

This commit is contained in:
Evan You 2019-10-29 12:30:09 -04:00
parent 5cce23f4c6
commit 17d71fa407
5 changed files with 507 additions and 436 deletions

View 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
}
}

View 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,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(

View 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
}

View File

@ -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<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

View 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,6 +190,8 @@ export function createVNode(
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspenseType(type)
? ShapeFlags.SUSPENSE
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)