refactor(suspense): make suspense tree-shakeable
This commit is contained in:
parent
5cce23f4c6
commit
17d71fa407
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user