import { VNode , normalizeVNode , VNodeProps , isSameVNodeType , openBlock , closeBlock , currentBlock , createVNode } 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 ) : boolean => type . __isSuspense ; export const SuspenseImpl = { name : 'Suspense' , __isSuspense : true , process ( n1 , n2 , container , anchor , parentComponent , parentSuspense , isSVG , slotScopeIds , optimized , rendererInternals ) { if ( n1 == null ) { mountSuspense ( n2 , container , anchor , parentComponent , parentSuspense , isSVG , slotScopeIds , optimized , rendererInternals ) ; } else { patchSuspense ( n1 , n2 , container , anchor , parentComponent , isSVG , slotScopeIds , optimized , rendererInternals ) ; } } , hydrate : hydrateSuspense , create : createSuspenseBoundary , normalize : normalizeSuspenseChildren } ; export const Suspense = ( __FEATURE_SUSPENSE__ ? SuspenseImpl : null ) as any as { __isSuspense : true ; new ( ) : { $props : VNodeProps & SuspenseProps ; } ; } ; function triggerEvent ( vnode , name ) { const eventListener = vnode . props && vnode . props [ name ] ; if ( isFunction ( eventListener ) ) { eventListener ( ) ; } } function mountSuspense ( vnode , container , anchor , parentComponent , parentSuspense , isSVG , slotScopeIds , optimized , rendererInternals ) { const { p : patch , o : { createElement } } = rendererInternals ; const hiddenContainer = createElement ( 'div' ) ; const suspense = ( vnode . suspense = createSuspenseBoundary ( vnode , parentSuspense , parentComponent , container , hiddenContainer , anchor , isSVG , slotScopeIds , optimized , rendererInternals ) ) ; patch ( null , ( suspense . pendingBranch = vnode . ssContent ! ) , hiddenContainer , null , parentComponent , suspense , isSVG , slotScopeIds ) ; if ( suspense . deps > 0 ) { triggerEvent ( vnode , 'onPending' ) ; triggerEvent ( vnode , 'onFallback' ) ; patch ( null , vnode . ssFallback ! , container , anchor , parentComponent , null , isSVG , slotScopeIds ) ; setActiveBranch ( suspense , vnode . ssFallback ! ) ; } else { suspense . resolve ( ) ; } } function patchSuspense ( n1 , n2 , container , anchor , parentComponent , isSVG , slotScopeIds , optimized , { p : patch , um : unmount , o : { createElement } } ) { 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 ) ) { patch ( pendingBranch , newBranch , suspense . hiddenContainer , null , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; if ( suspense . deps <= 0 ) { suspense . resolve ( ) ; } else if ( isInFallback ) { patch ( activeBranch , newFallback , container , anchor , parentComponent , null , isSVG , slotScopeIds , optimized ) ; setActiveBranch ( suspense , newFallback ) ; } } else { suspense . pendingId ++ ; if ( isHydrating ) { suspense . isHydrating = false ; suspense . activeBranch = pendingBranch ; } else { unmount ( pendingBranch , parentComponent , suspense ) ; } suspense . deps = 0 ; suspense . effects . length = 0 ; suspense . hiddenContainer = createElement ( 'div' ) ; if ( isInFallback ) { patch ( null , newBranch , suspense . hiddenContainer , null , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; if ( suspense . deps <= 0 ) { suspense . resolve ( ) ; } else { patch ( activeBranch , newFallback , container , anchor , parentComponent , null , isSVG , slotScopeIds , optimized ) ; setActiveBranch ( suspense , newFallback ) ; } } else if ( activeBranch && isSameVNodeType ( newBranch , activeBranch ) ) { patch ( activeBranch , newBranch , container , anchor , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; suspense . resolve ( true ) ; } else { patch ( null , newBranch , suspense . hiddenContainer , null , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; if ( suspense . deps <= 0 ) { suspense . resolve ( ) ; } } } } else { if ( activeBranch && isSameVNodeType ( newBranch , activeBranch ) ) { patch ( activeBranch , newBranch , container , anchor , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; setActiveBranch ( suspense , newBranch ) ; } else { triggerEvent ( n2 , 'onPending' ) ; suspense . pendingBranch = newBranch ; suspense . pendingId ++ ; patch ( null , newBranch , suspense . hiddenContainer , null , parentComponent , suspense , isSVG , slotScopeIds , optimized ) ; if ( suspense . deps <= 0 ) { 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 ? ) : void ; fallback ( fallbackVNode ) : void ; move ( container , anchor , type ) : void ; next ( ) : RendererNode | null ; registerDep ( instance , setupRenderEffect ) : void ; unmount ( parentSuspense , doRemove ? ) : void ; } let hasWarned = false ; function createSuspenseBoundary ( vnode , parent , parentComponent , container , hiddenContainer , anchor , isSVG , slotScopeIds , optimized , rendererInternals , isHydrating = false ) { if ( __DEV__ && ! __TEST__ && ! hasWarned ) { hasWarned = true ; 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 = { 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 ) ; } } ; } let { anchor } = suspense ; if ( activeBranch ) { anchor = next ( activeBranch ) ; unmount ( activeBranch , parentComponent , suspense , true ) ; } if ( ! delayEnter ) { move ( pendingBranch ! , container , anchor , MoveType . ENTER ) ; } } setActiveBranch ( suspense , pendingBranch ! ) ; suspense . pendingBranch = null ; suspense . isInFallback = false ; let parent = suspense . parent ; let hasUnresolvedAncestor = false ; while ( parent ) { if ( parent . pendingBranch ) { parent . effects . push ( ... effects ) ; hasUnresolvedAncestor = true ; break ; } parent = parent . parent ; } if ( ! hasUnresolvedAncestor ) { queuePostFlushCb ( effects ) ; } suspense . effects = [ ] ; triggerEvent ( vnode , 'onResolve' ) ; } , fallback ( fallbackVNode ) { if ( ! suspense . pendingBranch ) { return ; } const { vnode , activeBranch , parentComponent , container , isSVG } = suspense ; triggerEvent ( vnode , 'onFallback' ) ; const anchor = next ( activeBranch ! ) ; const mountFallback = ( ) => { if ( ! suspense . isInFallback ) { return ; } patch ( null , fallbackVNode , container , anchor , parentComponent , null , isSVG , slotScopeIds , optimized ) ; setActiveBranch ( suspense , fallbackVNode ) ; } ; const delayEnter = fallbackVNode . transition && fallbackVNode . transition . mode === 'out-in' ; if ( delayEnter ) { activeBranch ! . transition ! . afterLeave = mountFallback ; } suspense . isInFallback = true ; unmount ( activeBranch ! , parentComponent , null , 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 => { if ( instance . isUnmounted || suspense . isUnmounted || suspense . pendingId !== instance . suspenseId ) { return ; } instance . asyncResolved = true ; const { vnode } = instance ; if ( __DEV__ ) { pushWarningContext ( vnode ) ; } handleSetupResult ( instance , asyncSetupResult , false ) ; if ( hydratedEl ) { vnode . el = hydratedEl ; } const placeholder = ! hydratedEl && instance . subTree . el ; setupRenderEffect ( instance , vnode , parentNode ( hydratedEl || instance . subTree . el ! ) ! , hydratedEl ? null : next ( instance . subTree ) , suspense , isSVG , optimized ) ; if ( placeholder ) { remove ( placeholder ) ; } updateHOCHostEl ( instance , vnode . el ) ; if ( __DEV__ ) { popWarningContext ( ) ; } 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 , vnode , parentComponent , parentSuspense , isSVG , slotScopeIds , optimized , rendererInternals , hydrateNode ) { const suspense = ( vnode . suspense = createSuspenseBoundary ( vnode , parentSuspense , parentComponent , node . parentNode ! , document . createElement ( 'div' ) , null , isSVG , slotScopeIds , optimized , rendererInternals , true ) ) ; const result = hydrateNode ( node , ( suspense . pendingBranch = vnode . ssContent ! ) , parentComponent , suspense , slotScopeIds , optimized ) ; if ( suspense . deps === 0 ) { suspense . resolve ( ) ; } return result ; } function normalizeSuspenseChildren ( vnode ) { const { shapeFlag , children } = vnode ; const isSlotChildren = shapeFlag & ShapeFlags . SLOTS_CHILDREN ; vnode . ssContent = normalizeSuspenseSlot ( isSlotChildren ? ( children as Slots ) . default : children ) ; vnode . ssFallback = isSlotChildren ? normalizeSuspenseSlot ( ( children as Slots ) . fallback ) : createVNode ( Comment ) ; } function normalizeSuspenseSlot ( s ) { let block ; if ( isFunction ( s ) ) { const isCompiledSlot = s . _c ; if ( isCompiledSlot ) { s . _d = false ; openBlock ( ) ; } s = s ( ) ; if ( isCompiledSlot ) { s . _d = true ; block = currentBlock ; closeBlock ( ) ; } } if ( isArray ( s ) ) { const singleChild = filterSingleRoot ( s ) ; if ( __DEV__ && ! singleChild ) { warn ( `<Suspense> slots expect a single root node.` ) ; } s = singleChild ; } s = normalizeVNode ( s ) ; if ( block && ! s . dynamicChildren ) { s . dynamicChildren = block . filter ( c => c !== s ) ; } return s ; } export function queueEffectWithSuspense ( fn , suspense ) { if ( suspense && suspense . pendingBranch ) { if ( isArray ( fn ) ) { suspense . effects . push ( ... fn ) ; } else { suspense . effects . push ( fn ) ; } } else { queuePostFlushCb ( fn ) ; } } function setActiveBranch ( suspense , branch ) { suspense . activeBranch = branch ; const { vnode , parentComponent } = suspense ; const el = ( vnode . el = branch . el ) ; if ( parentComponent && parentComponent . subTree === vnode ) { parentComponent . vnode . el = el ; updateHOCHostEl ( parentComponent , el ) ; } }