| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739 |
- import {
- VNode,
- normalizeVNode,
- VNodeChild,
- VNodeProps,
- isSameVNodeType
- } from '../vnode'
- import { isFunction, isArray, ShapeFlags, toNumber } from '@vue/shared'
- import { ComponentInternalInstance, handleSetupResult } from '../component'
- import { Slots } from '../componentSlots'
- import {
- RendererInternals,
- MoveType,
- SetupRenderEffectFn,
- RendererNode,
- RendererElement
- } from '../renderer'
- import { queuePostFlushCb } from '../scheduler'
- import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
- import { pushWarningContext, popWarningContext, warn } from '../warning'
- import { handleError, ErrorCodes } from '../errorHandling'
- export interface SuspenseProps {
- onResolve?: () => void
- onPending?: () => void
- onFallback?: () => void
- timeout?: string | number
- }
- export const isSuspense = (type: any): boolean => type.__isSuspense
- // Suspense exposes a component-like API, and is treated like a component
- // in the compiler, but internally it's a special built-in type that hooks
- // directly into the renderer.
- export const SuspenseImpl = {
- // In order to make Suspense tree-shakable, we need to avoid importing it
- // directly in the renderer. The renderer checks for the __isSuspense flag
- // on a vnode's type and calls the `process` method, passing in renderer
- // internals.
- __isSuspense: true,
- process(
- n1: VNode | null,
- n2: VNode,
- container: RendererElement,
- anchor: RendererNode | 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,
- rendererInternals
- )
- }
- },
- hydrate: hydrateSuspense,
- create: createSuspenseBoundary
- }
- // Force-casted public typing for h and TSX props inference
- export const Suspense = ((__FEATURE_SUSPENSE__
- ? SuspenseImpl
- : null) as any) as {
- __isSuspense: true
- new (): { $props: VNodeProps & SuspenseProps }
- }
- function mountSuspense(
- vnode: VNode,
- container: RendererElement,
- anchor: RendererNode | null,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
- optimized: boolean,
- rendererInternals: RendererInternals
- ) {
- const {
- p: patch,
- o: { createElement }
- } = rendererInternals
- const hiddenContainer = createElement('div')
- const suspense = (vnode.suspense = createSuspenseBoundary(
- vnode,
- parentSuspense,
- parentComponent,
- container,
- hiddenContainer,
- anchor,
- isSVG,
- optimized,
- rendererInternals
- ))
- // start mounting the content subtree in an off-dom container
- patch(
- null,
- (suspense.pendingBranch = vnode.ssContent!),
- hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG
- )
- // now check if we have encountered any async deps
- if (suspense.deps > 0) {
- // has async
- // mount the fallback tree
- patch(
- null,
- vnode.ssFallback!,
- container,
- anchor,
- parentComponent,
- null, // fallback tree will not have suspense context
- isSVG
- )
- setActiveBranch(suspense, vnode.ssFallback!)
- } else {
- // Suspense has no async deps. Just resolve.
- suspense.resolve()
- }
- }
- function patchSuspense(
- n1: VNode,
- n2: VNode,
- container: RendererElement,
- anchor: RendererNode | null,
- parentComponent: ComponentInternalInstance | null,
- isSVG: boolean,
- { p: patch, um: unmount, o: { createElement } }: RendererInternals
- ) {
- const suspense = (n2.suspense = n1.suspense)!
- suspense.vnode = n2
- n2.el = n1.el
- const newBranch = n2.ssContent!
- const newFallback = n2.ssFallback!
- const { activeBranch, pendingBranch, isInFallback, isHydrating } = suspense
- if (pendingBranch) {
- suspense.pendingBranch = newBranch
- if (isSameVNodeType(newBranch, pendingBranch)) {
- // same root type but content may have changed.
- patch(
- pendingBranch,
- newBranch,
- suspense.hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG
- )
- if (suspense.deps <= 0) {
- suspense.resolve()
- } else if (isInFallback) {
- patch(
- activeBranch,
- newFallback,
- container,
- anchor,
- parentComponent,
- null, // fallback tree will not have suspense context
- isSVG
- )
- setActiveBranch(suspense, newFallback)
- }
- } else {
- // toggled before pending tree is resolved
- suspense.pendingId++
- if (isHydrating) {
- // if toggled before hydration is finished, the current DOM tree is
- // no longer valid. set it as the active branch so it will be unmounted
- // when resolved
- suspense.isHydrating = false
- suspense.activeBranch = pendingBranch
- } else {
- unmount(pendingBranch, parentComponent, suspense)
- }
- // increment pending ID. this is used to invalidate async callbacks
- // reset suspense state
- suspense.deps = 0
- // discard effects from pending branch
- suspense.effects.length = 0
- // discard previous container
- suspense.hiddenContainer = createElement('div')
- if (isInFallback) {
- // already in fallback state
- patch(
- null,
- newBranch,
- suspense.hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG
- )
- if (suspense.deps <= 0) {
- suspense.resolve()
- } else {
- patch(
- activeBranch,
- newFallback,
- container,
- anchor,
- parentComponent,
- null, // fallback tree will not have suspense context
- isSVG
- )
- setActiveBranch(suspense, newFallback)
- }
- } else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
- // toggled "back" to current active branch
- patch(
- activeBranch,
- newBranch,
- container,
- anchor,
- parentComponent,
- suspense,
- isSVG
- )
- // force resolve
- suspense.resolve(true)
- } else {
- // switched to a 3rd branch
- patch(
- null,
- newBranch,
- suspense.hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG
- )
- if (suspense.deps <= 0) {
- suspense.resolve()
- }
- }
- }
- } else {
- if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
- // root did not change, just normal patch
- patch(
- activeBranch,
- newBranch,
- container,
- anchor,
- parentComponent,
- suspense,
- isSVG
- )
- setActiveBranch(suspense, newBranch)
- } else {
- // root node toggled
- // invoke @pending event
- const onPending = n2.props && n2.props.onPending
- if (isFunction(onPending)) {
- onPending()
- }
- // mount pending branch in off-dom container
- suspense.pendingBranch = newBranch
- suspense.pendingId++
- patch(
- null,
- newBranch,
- suspense.hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG
- )
- if (suspense.deps <= 0) {
- // incoming branch has no async deps, resolve now.
- suspense.resolve()
- } else {
- const { timeout, pendingId } = suspense
- if (timeout > 0) {
- setTimeout(() => {
- if (suspense.pendingId === pendingId) {
- suspense.fallback(newFallback)
- }
- }, timeout)
- } else if (timeout === 0) {
- suspense.fallback(newFallback)
- }
- }
- }
- }
- }
- export interface SuspenseBoundary {
- vnode: VNode<RendererNode, RendererElement, SuspenseProps>
- parent: SuspenseBoundary | null
- parentComponent: ComponentInternalInstance | null
- isSVG: boolean
- container: RendererElement
- hiddenContainer: RendererElement
- anchor: RendererNode | null
- activeBranch: VNode | null
- pendingBranch: VNode | null
- deps: number
- pendingId: number
- timeout: number
- isInFallback: boolean
- isHydrating: boolean
- isUnmounted: boolean
- effects: Function[]
- resolve(force?: boolean): void
- fallback(fallbackVNode: VNode): void
- move(
- container: RendererElement,
- anchor: RendererNode | null,
- type: MoveType
- ): void
- next(): RendererNode | null
- registerDep(
- instance: ComponentInternalInstance,
- setupRenderEffect: SetupRenderEffectFn
- ): void
- unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
- }
- let hasWarned = false
- function createSuspenseBoundary(
- vnode: VNode,
- parent: SuspenseBoundary | null,
- parentComponent: ComponentInternalInstance | null,
- container: RendererElement,
- hiddenContainer: RendererElement,
- anchor: RendererNode | null,
- isSVG: boolean,
- optimized: boolean,
- rendererInternals: RendererInternals,
- isHydrating = false
- ): SuspenseBoundary {
- /* istanbul ignore if */
- if (__DEV__ && !__TEST__ && !hasWarned) {
- hasWarned = true
- // @ts-ignore `console.info` cannot be null error
- console[console.info ? 'info' : 'log'](
- `<Suspense> is an experimental feature and its API will likely change.`
- )
- }
- const {
- p: patch,
- m: move,
- um: unmount,
- n: next,
- o: { parentNode, remove }
- } = rendererInternals
- const timeout = toNumber(vnode.props && vnode.props.timeout)
- const suspense: SuspenseBoundary = {
- vnode,
- parent,
- parentComponent,
- isSVG,
- container,
- hiddenContainer,
- anchor,
- deps: 0,
- pendingId: 0,
- timeout: typeof timeout === 'number' ? timeout : -1,
- activeBranch: null,
- pendingBranch: null,
- isInFallback: true,
- isHydrating,
- isUnmounted: false,
- effects: [],
- resolve(resume = false) {
- if (__DEV__) {
- if (!resume && !suspense.pendingBranch) {
- throw new Error(
- `suspense.resolve() is called without a pending branch.`
- )
- }
- if (suspense.isUnmounted) {
- throw new Error(
- `suspense.resolve() is called on an already unmounted suspense boundary.`
- )
- }
- }
- const {
- vnode,
- activeBranch,
- pendingBranch,
- pendingId,
- effects,
- parentComponent,
- container
- } = suspense
- if (suspense.isHydrating) {
- suspense.isHydrating = false
- } else if (!resume) {
- const delayEnter =
- activeBranch &&
- pendingBranch!.transition &&
- pendingBranch!.transition.mode === 'out-in'
- if (delayEnter) {
- activeBranch!.transition!.afterLeave = () => {
- if (pendingId === suspense.pendingId) {
- move(pendingBranch!, container, anchor, MoveType.ENTER)
- }
- }
- }
- // this is initial anchor on mount
- let { anchor } = suspense
- // unmount current active tree
- if (activeBranch) {
- // 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(activeBranch)
- unmount(activeBranch, parentComponent, suspense, true)
- }
- if (!delayEnter) {
- // move content from off-dom container to actual container
- move(pendingBranch!, container, anchor, MoveType.ENTER)
- }
- }
- setActiveBranch(suspense, pendingBranch!)
- suspense.pendingBranch = null
- suspense.isInFallback = false
- // flush buffered effects
- // check if there is a pending parent suspense
- let parent = suspense.parent
- let hasUnresolvedAncestor = false
- while (parent) {
- if (parent.pendingBranch) {
- // 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.effects = []
- // invoke @resolve event
- const onResolve = vnode.props && vnode.props.onResolve
- if (isFunction(onResolve)) {
- onResolve()
- }
- },
- fallback(fallbackVNode) {
- if (!suspense.pendingBranch) {
- return
- }
- const {
- vnode,
- activeBranch,
- parentComponent,
- container,
- isSVG
- } = suspense
- // invoke @fallback event
- const onFallback = vnode.props && vnode.props.onFallback
- if (isFunction(onFallback)) {
- onFallback()
- }
- const anchor = next(activeBranch!)
- const mountFallback = () => {
- if (!suspense.isInFallback) {
- return
- }
- // mount the fallback tree
- patch(
- null,
- fallbackVNode,
- container,
- anchor,
- parentComponent,
- null, // fallback tree will not have suspense context
- isSVG
- )
- setActiveBranch(suspense, fallbackVNode)
- }
- const delayEnter =
- fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
- if (delayEnter) {
- activeBranch!.transition!.afterLeave = mountFallback
- }
- // unmount current active branch
- unmount(
- activeBranch!,
- parentComponent,
- null, // no suspense so unmount hooks fire now
- true // shouldRemove
- )
- suspense.isInFallback = true
- if (!delayEnter) {
- mountFallback()
- }
- },
- move(container, anchor, type) {
- suspense.activeBranch &&
- move(suspense.activeBranch, container, anchor, type)
- suspense.container = container
- },
- next() {
- return suspense.activeBranch && next(suspense.activeBranch)
- },
- registerDep(instance, setupRenderEffect) {
- const isInPendingSuspense = !!suspense.pendingBranch
- if (isInPendingSuspense) {
- suspense.deps++
- }
- const hydratedEl = instance.vnode.el
- 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 ||
- suspense.pendingId !== instance.suspenseId
- ) {
- return
- }
- // retry from this component
- instance.asyncResolved = true
- const { vnode } = instance
- if (__DEV__) {
- pushWarningContext(vnode)
- }
- handleSetupResult(instance, asyncSetupResult, false)
- if (hydratedEl) {
- // vnode may have been replaced if an update happened before the
- // async dep is resolved.
- vnode.el = hydratedEl
- }
- const placeholder = !hydratedEl && instance.subTree.el
- setupRenderEffect(
- instance,
- vnode,
- // component may have been moved before resolve.
- // if this is not a hydration, instance.subTree will be the comment
- // placeholder.
- parentNode(hydratedEl || instance.subTree.el!)!,
- // anchor will not be used if this is hydration, so only need to
- // consider the comment placeholder case.
- hydratedEl ? null : next(instance.subTree),
- suspense,
- isSVG,
- optimized
- )
- if (placeholder) {
- remove(placeholder)
- }
- updateHOCHostEl(instance, vnode.el)
- if (__DEV__) {
- popWarningContext()
- }
- // only decrease deps count if suspense is not already resolved
- if (isInPendingSuspense && --suspense.deps === 0) {
- suspense.resolve()
- }
- })
- },
- unmount(parentSuspense, doRemove) {
- suspense.isUnmounted = true
- if (suspense.activeBranch) {
- unmount(
- suspense.activeBranch,
- parentComponent,
- parentSuspense,
- doRemove
- )
- }
- if (suspense.pendingBranch) {
- unmount(
- suspense.pendingBranch,
- parentComponent,
- parentSuspense,
- doRemove
- )
- }
- }
- }
- return suspense
- }
- function hydrateSuspense(
- node: Node,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
- optimized: boolean,
- rendererInternals: RendererInternals,
- hydrateNode: (
- node: Node,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- optimized: boolean
- ) => Node | null
- ): Node | null {
- /* eslint-disable no-restricted-globals */
- const suspense = (vnode.suspense = createSuspenseBoundary(
- vnode,
- parentSuspense,
- parentComponent,
- node.parentNode!,
- document.createElement('div'),
- null,
- isSVG,
- optimized,
- rendererInternals,
- true /* hydrating */
- ))
- // there are two possible scenarios for server-rendered suspense:
- // - success: ssr content should be fully resolved
- // - failure: ssr content should be the fallback branch.
- // however, on the client we don't really know if it has failed or not
- // attempt to hydrate the DOM assuming it has succeeded, but we still
- // need to construct a suspense boundary first
- const result = hydrateNode(
- node,
- (suspense.pendingBranch = vnode.ssContent!),
- parentComponent,
- suspense,
- optimized
- )
- if (suspense.deps === 0) {
- suspense.resolve()
- }
- return result
- /* eslint-enable no-restricted-globals */
- }
- export function normalizeSuspenseChildren(
- vnode: VNode
- ): {
- content: VNode
- fallback: VNode
- } {
- const { shapeFlag, children } = vnode
- let content: VNode
- let fallback: VNode
- if (shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
- content = normalizeSuspenseSlot((children as Slots).default)
- fallback = normalizeSuspenseSlot((children as Slots).fallback)
- } else {
- content = normalizeSuspenseSlot(children as VNodeChild)
- fallback = normalizeVNode(null)
- }
- return {
- content,
- fallback
- }
- }
- function normalizeSuspenseSlot(s: any) {
- if (isFunction(s)) {
- s = s()
- }
- if (isArray(s)) {
- const singleChild = filterSingleRoot(s)
- if (__DEV__ && !singleChild) {
- warn(`<Suspense> slots expect a single root node.`)
- }
- s = singleChild
- }
- return normalizeVNode(s)
- }
- export function queueEffectWithSuspense(
- fn: Function | Function[],
- suspense: SuspenseBoundary | null
- ): void {
- if (suspense && suspense.pendingBranch) {
- if (isArray(fn)) {
- suspense.effects.push(...fn)
- } else {
- suspense.effects.push(fn)
- }
- } else {
- queuePostFlushCb(fn)
- }
- }
- function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
- suspense.activeBranch = branch
- const { vnode, parentComponent } = suspense
- const el = (vnode.el = branch.el)
- // in case suspense is the root node of a component,
- // recursively update the HOC el
- if (parentComponent && parentComponent.subTree === vnode) {
- parentComponent.vnode.el = el
- updateHOCHostEl(parentComponent, el)
- }
- }
|