Suspense.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. import {
  2. Comment,
  3. type VNode,
  4. type VNodeProps,
  5. closeBlock,
  6. createVNode,
  7. currentBlock,
  8. isBlockTreeEnabled,
  9. isSameVNodeType,
  10. normalizeVNode,
  11. openBlock,
  12. } from '../vnode'
  13. import { ShapeFlags, isArray, isFunction, toNumber } from '@vue/shared'
  14. import { type ComponentInternalInstance, handleSetupResult } from '../component'
  15. import type { Slots } from '../componentSlots'
  16. import {
  17. type ElementNamespace,
  18. MoveType,
  19. type RendererElement,
  20. type RendererInternals,
  21. type RendererNode,
  22. type SetupRenderEffectFn,
  23. } from '../renderer'
  24. import { queuePostFlushCb } from '../scheduler'
  25. import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
  26. import {
  27. assertNumber,
  28. popWarningContext,
  29. pushWarningContext,
  30. warn,
  31. } from '../warning'
  32. import { ErrorCodes, handleError } from '../errorHandling'
  33. import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets'
  34. export interface SuspenseProps {
  35. onResolve?: () => void
  36. onPending?: () => void
  37. onFallback?: () => void
  38. timeout?: string | number
  39. /**
  40. * Allow suspense to be captured by parent suspense
  41. *
  42. * @default false
  43. */
  44. suspensible?: boolean
  45. }
  46. export const isSuspense = (type: any): boolean => type.__isSuspense
  47. // incrementing unique id for every pending branch
  48. let suspenseId = 0
  49. /**
  50. * For testing only
  51. */
  52. export const resetSuspenseId = () => (suspenseId = 0)
  53. // Suspense exposes a component-like API, and is treated like a component
  54. // in the compiler, but internally it's a special built-in type that hooks
  55. // directly into the renderer.
  56. export const SuspenseImpl = {
  57. name: 'Suspense',
  58. // In order to make Suspense tree-shakable, we need to avoid importing it
  59. // directly in the renderer. The renderer checks for the __isSuspense flag
  60. // on a vnode's type and calls the `process` method, passing in renderer
  61. // internals.
  62. __isSuspense: true,
  63. process(
  64. n1: VNode | null,
  65. n2: VNode,
  66. container: RendererElement,
  67. anchor: RendererNode | null,
  68. parentComponent: ComponentInternalInstance | null,
  69. parentSuspense: SuspenseBoundary | null,
  70. namespace: ElementNamespace,
  71. slotScopeIds: string[] | null,
  72. optimized: boolean,
  73. // platform-specific impl passed from renderer
  74. rendererInternals: RendererInternals,
  75. ) {
  76. if (n1 == null) {
  77. mountSuspense(
  78. n2,
  79. container,
  80. anchor,
  81. parentComponent,
  82. parentSuspense,
  83. namespace,
  84. slotScopeIds,
  85. optimized,
  86. rendererInternals,
  87. )
  88. } else {
  89. // #8678 if the current suspense needs to be patched and parentSuspense has
  90. // not been resolved. this means that both the current suspense and parentSuspense
  91. // need to be patched. because parentSuspense's pendingBranch includes the
  92. // current suspense, it will be processed twice:
  93. // 1. current patch
  94. // 2. mounting along with the pendingBranch of parentSuspense
  95. // it is necessary to skip the current patch to avoid multiple mounts
  96. // of inner components.
  97. if (
  98. parentSuspense &&
  99. parentSuspense.deps > 0 &&
  100. !n1.suspense!.isInFallback
  101. ) {
  102. n2.suspense = n1.suspense!
  103. n2.suspense.vnode = n2
  104. n2.el = n1.el
  105. return
  106. }
  107. patchSuspense(
  108. n1,
  109. n2,
  110. container,
  111. anchor,
  112. parentComponent,
  113. namespace,
  114. slotScopeIds,
  115. optimized,
  116. rendererInternals,
  117. )
  118. }
  119. },
  120. hydrate: hydrateSuspense,
  121. create: createSuspenseBoundary,
  122. normalize: normalizeSuspenseChildren,
  123. }
  124. // Force-casted public typing for h and TSX props inference
  125. export const Suspense = (__FEATURE_SUSPENSE__
  126. ? SuspenseImpl
  127. : null) as unknown as {
  128. __isSuspense: true
  129. new (): {
  130. $props: VNodeProps & SuspenseProps
  131. $slots: {
  132. default(): VNode[]
  133. fallback(): VNode[]
  134. }
  135. }
  136. }
  137. function triggerEvent(
  138. vnode: VNode,
  139. name: 'onResolve' | 'onPending' | 'onFallback',
  140. ) {
  141. const eventListener = vnode.props && vnode.props[name]
  142. if (isFunction(eventListener)) {
  143. eventListener()
  144. }
  145. }
  146. function mountSuspense(
  147. vnode: VNode,
  148. container: RendererElement,
  149. anchor: RendererNode | null,
  150. parentComponent: ComponentInternalInstance | null,
  151. parentSuspense: SuspenseBoundary | null,
  152. namespace: ElementNamespace,
  153. slotScopeIds: string[] | null,
  154. optimized: boolean,
  155. rendererInternals: RendererInternals,
  156. ) {
  157. const {
  158. p: patch,
  159. o: { createElement },
  160. } = rendererInternals
  161. const hiddenContainer = createElement('div')
  162. const suspense = (vnode.suspense = createSuspenseBoundary(
  163. vnode,
  164. parentSuspense,
  165. parentComponent,
  166. container,
  167. hiddenContainer,
  168. anchor,
  169. namespace,
  170. slotScopeIds,
  171. optimized,
  172. rendererInternals,
  173. ))
  174. // start mounting the content subtree in an off-dom container
  175. patch(
  176. null,
  177. (suspense.pendingBranch = vnode.ssContent!),
  178. hiddenContainer,
  179. null,
  180. parentComponent,
  181. suspense,
  182. namespace,
  183. slotScopeIds,
  184. )
  185. // now check if we have encountered any async deps
  186. if (suspense.deps > 0) {
  187. // has async
  188. // invoke @fallback event
  189. triggerEvent(vnode, 'onPending')
  190. triggerEvent(vnode, 'onFallback')
  191. // mount the fallback tree
  192. patch(
  193. null,
  194. vnode.ssFallback!,
  195. container,
  196. anchor,
  197. parentComponent,
  198. null, // fallback tree will not have suspense context
  199. namespace,
  200. slotScopeIds,
  201. )
  202. setActiveBranch(suspense, vnode.ssFallback!)
  203. } else {
  204. // Suspense has no async deps. Just resolve.
  205. suspense.resolve(false, true)
  206. }
  207. }
  208. function patchSuspense(
  209. n1: VNode,
  210. n2: VNode,
  211. container: RendererElement,
  212. anchor: RendererNode | null,
  213. parentComponent: ComponentInternalInstance | null,
  214. namespace: ElementNamespace,
  215. slotScopeIds: string[] | null,
  216. optimized: boolean,
  217. { p: patch, um: unmount, o: { createElement } }: RendererInternals,
  218. ) {
  219. const suspense = (n2.suspense = n1.suspense)!
  220. suspense.vnode = n2
  221. n2.el = n1.el
  222. const newBranch = n2.ssContent!
  223. const newFallback = n2.ssFallback!
  224. const { activeBranch, pendingBranch, isInFallback, isHydrating } = suspense
  225. if (pendingBranch) {
  226. suspense.pendingBranch = newBranch
  227. if (isSameVNodeType(newBranch, pendingBranch)) {
  228. // same root type but content may have changed.
  229. patch(
  230. pendingBranch,
  231. newBranch,
  232. suspense.hiddenContainer,
  233. null,
  234. parentComponent,
  235. suspense,
  236. namespace,
  237. slotScopeIds,
  238. optimized,
  239. )
  240. if (suspense.deps <= 0) {
  241. suspense.resolve()
  242. } else if (isInFallback) {
  243. // It's possible that the app is in hydrating state when patching the
  244. // suspense instance. If someone updates the dependency during component
  245. // setup in children of suspense boundary, that would be problemtic
  246. // because we aren't actually showing a fallback content when
  247. // patchSuspense is called. In such case, patch of fallback content
  248. // should be no op
  249. if (!isHydrating) {
  250. patch(
  251. activeBranch,
  252. newFallback,
  253. container,
  254. anchor,
  255. parentComponent,
  256. null, // fallback tree will not have suspense context
  257. namespace,
  258. slotScopeIds,
  259. optimized,
  260. )
  261. setActiveBranch(suspense, newFallback)
  262. }
  263. }
  264. } else {
  265. // toggled before pending tree is resolved
  266. // increment pending ID. this is used to invalidate async callbacks
  267. suspense.pendingId = suspenseId++
  268. if (isHydrating) {
  269. // if toggled before hydration is finished, the current DOM tree is
  270. // no longer valid. set it as the active branch so it will be unmounted
  271. // when resolved
  272. suspense.isHydrating = false
  273. suspense.activeBranch = pendingBranch
  274. } else {
  275. unmount(pendingBranch, parentComponent, suspense)
  276. }
  277. // reset suspense state
  278. suspense.deps = 0
  279. // discard effects from pending branch
  280. suspense.effects.length = 0
  281. // discard previous container
  282. suspense.hiddenContainer = createElement('div')
  283. if (isInFallback) {
  284. // already in fallback state
  285. patch(
  286. null,
  287. newBranch,
  288. suspense.hiddenContainer,
  289. null,
  290. parentComponent,
  291. suspense,
  292. namespace,
  293. slotScopeIds,
  294. optimized,
  295. )
  296. if (suspense.deps <= 0) {
  297. suspense.resolve()
  298. } else {
  299. patch(
  300. activeBranch,
  301. newFallback,
  302. container,
  303. anchor,
  304. parentComponent,
  305. null, // fallback tree will not have suspense context
  306. namespace,
  307. slotScopeIds,
  308. optimized,
  309. )
  310. setActiveBranch(suspense, newFallback)
  311. }
  312. } else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
  313. // toggled "back" to current active branch
  314. patch(
  315. activeBranch,
  316. newBranch,
  317. container,
  318. anchor,
  319. parentComponent,
  320. suspense,
  321. namespace,
  322. slotScopeIds,
  323. optimized,
  324. )
  325. // force resolve
  326. suspense.resolve(true)
  327. } else {
  328. // switched to a 3rd branch
  329. patch(
  330. null,
  331. newBranch,
  332. suspense.hiddenContainer,
  333. null,
  334. parentComponent,
  335. suspense,
  336. namespace,
  337. slotScopeIds,
  338. optimized,
  339. )
  340. if (suspense.deps <= 0) {
  341. suspense.resolve()
  342. }
  343. }
  344. }
  345. } else {
  346. if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
  347. // root did not change, just normal patch
  348. patch(
  349. activeBranch,
  350. newBranch,
  351. container,
  352. anchor,
  353. parentComponent,
  354. suspense,
  355. namespace,
  356. slotScopeIds,
  357. optimized,
  358. )
  359. setActiveBranch(suspense, newBranch)
  360. } else {
  361. // root node toggled
  362. // invoke @pending event
  363. triggerEvent(n2, 'onPending')
  364. // mount pending branch in off-dom container
  365. suspense.pendingBranch = newBranch
  366. if (newBranch.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  367. suspense.pendingId = newBranch.component!.suspenseId!
  368. } else {
  369. suspense.pendingId = suspenseId++
  370. }
  371. patch(
  372. null,
  373. newBranch,
  374. suspense.hiddenContainer,
  375. null,
  376. parentComponent,
  377. suspense,
  378. namespace,
  379. slotScopeIds,
  380. optimized,
  381. )
  382. if (suspense.deps <= 0) {
  383. // incoming branch has no async deps, resolve now.
  384. suspense.resolve()
  385. } else {
  386. const { timeout, pendingId } = suspense
  387. if (timeout > 0) {
  388. setTimeout(() => {
  389. if (suspense.pendingId === pendingId) {
  390. suspense.fallback(newFallback)
  391. }
  392. }, timeout)
  393. } else if (timeout === 0) {
  394. suspense.fallback(newFallback)
  395. }
  396. }
  397. }
  398. }
  399. }
  400. export interface SuspenseBoundary {
  401. vnode: VNode<RendererNode, RendererElement, SuspenseProps>
  402. parent: SuspenseBoundary | null
  403. parentComponent: ComponentInternalInstance | null
  404. namespace: ElementNamespace
  405. container: RendererElement
  406. hiddenContainer: RendererElement
  407. activeBranch: VNode | null
  408. pendingBranch: VNode | null
  409. deps: number
  410. pendingId: number
  411. timeout: number
  412. isInFallback: boolean
  413. isHydrating: boolean
  414. isUnmounted: boolean
  415. effects: Function[]
  416. resolve(force?: boolean, sync?: boolean): void
  417. fallback(fallbackVNode: VNode): void
  418. move(
  419. container: RendererElement,
  420. anchor: RendererNode | null,
  421. type: MoveType,
  422. ): void
  423. next(): RendererNode | null
  424. registerDep(
  425. instance: ComponentInternalInstance,
  426. setupRenderEffect: SetupRenderEffectFn,
  427. ): void
  428. unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
  429. }
  430. let hasWarned = false
  431. function createSuspenseBoundary(
  432. vnode: VNode,
  433. parentSuspense: SuspenseBoundary | null,
  434. parentComponent: ComponentInternalInstance | null,
  435. container: RendererElement,
  436. hiddenContainer: RendererElement,
  437. anchor: RendererNode | null,
  438. namespace: ElementNamespace,
  439. slotScopeIds: string[] | null,
  440. optimized: boolean,
  441. rendererInternals: RendererInternals,
  442. isHydrating = false,
  443. ): SuspenseBoundary {
  444. /* istanbul ignore if */
  445. if (__DEV__ && !__TEST__ && !hasWarned) {
  446. hasWarned = true
  447. // @ts-expect-error `console.info` cannot be null error
  448. // eslint-disable-next-line no-console
  449. console[console.info ? 'info' : 'log'](
  450. `<Suspense> is an experimental feature and its API will likely change.`,
  451. )
  452. }
  453. const {
  454. p: patch,
  455. m: move,
  456. um: unmount,
  457. n: next,
  458. o: { parentNode, remove },
  459. } = rendererInternals
  460. // if set `suspensible: true`, set the current suspense as a dep of parent suspense
  461. let parentSuspenseId: number | undefined
  462. const isSuspensible = isVNodeSuspensible(vnode)
  463. if (isSuspensible) {
  464. if (parentSuspense?.pendingBranch) {
  465. parentSuspenseId = parentSuspense.pendingId
  466. parentSuspense.deps++
  467. }
  468. }
  469. const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined
  470. if (__DEV__) {
  471. assertNumber(timeout, `Suspense timeout`)
  472. }
  473. const initialAnchor = anchor
  474. const suspense: SuspenseBoundary = {
  475. vnode,
  476. parent: parentSuspense,
  477. parentComponent,
  478. namespace,
  479. container,
  480. hiddenContainer,
  481. deps: 0,
  482. pendingId: suspenseId++,
  483. timeout: typeof timeout === 'number' ? timeout : -1,
  484. activeBranch: null,
  485. pendingBranch: null,
  486. isInFallback: !isHydrating,
  487. isHydrating,
  488. isUnmounted: false,
  489. effects: [],
  490. resolve(resume = false, sync = false) {
  491. if (__DEV__) {
  492. if (!resume && !suspense.pendingBranch) {
  493. throw new Error(
  494. `suspense.resolve() is called without a pending branch.`,
  495. )
  496. }
  497. if (suspense.isUnmounted) {
  498. throw new Error(
  499. `suspense.resolve() is called on an already unmounted suspense boundary.`,
  500. )
  501. }
  502. }
  503. const {
  504. vnode,
  505. activeBranch,
  506. pendingBranch,
  507. pendingId,
  508. effects,
  509. parentComponent,
  510. container,
  511. } = suspense
  512. // if there's a transition happening we need to wait it to finish.
  513. let delayEnter: boolean | null = false
  514. if (suspense.isHydrating) {
  515. suspense.isHydrating = false
  516. } else if (!resume) {
  517. delayEnter =
  518. activeBranch &&
  519. pendingBranch!.transition &&
  520. pendingBranch!.transition.mode === 'out-in'
  521. if (delayEnter) {
  522. activeBranch!.transition!.afterLeave = () => {
  523. if (pendingId === suspense.pendingId) {
  524. move(
  525. pendingBranch!,
  526. container,
  527. anchor === initialAnchor ? next(activeBranch!) : anchor,
  528. MoveType.ENTER,
  529. )
  530. queuePostFlushCb(effects)
  531. }
  532. }
  533. }
  534. // unmount current active tree
  535. if (activeBranch) {
  536. // if the fallback tree was mounted, it may have been moved
  537. // as part of a parent suspense. get the latest anchor for insertion
  538. // #8105 if `delayEnter` is true, it means that the mounting of
  539. // `activeBranch` will be delayed. if the branch switches before
  540. // transition completes, both `activeBranch` and `pendingBranch` may
  541. // coexist in the `hiddenContainer`. This could result in
  542. // `next(activeBranch!)` obtaining an incorrect anchor
  543. // (got `pendingBranch.el`).
  544. // Therefore, after the mounting of activeBranch is completed,
  545. // it is necessary to get the latest anchor.
  546. if (parentNode(activeBranch.el!) !== suspense.hiddenContainer) {
  547. anchor = next(activeBranch)
  548. }
  549. unmount(activeBranch, parentComponent, suspense, true)
  550. }
  551. if (!delayEnter) {
  552. // move content from off-dom container to actual container
  553. move(pendingBranch!, container, anchor, MoveType.ENTER)
  554. }
  555. }
  556. setActiveBranch(suspense, pendingBranch!)
  557. suspense.pendingBranch = null
  558. suspense.isInFallback = false
  559. // flush buffered effects
  560. // check if there is a pending parent suspense
  561. let parent = suspense.parent
  562. let hasUnresolvedAncestor = false
  563. while (parent) {
  564. if (parent.pendingBranch) {
  565. // found a pending parent suspense, merge buffered post jobs
  566. // into that parent
  567. parent.effects.push(...effects)
  568. hasUnresolvedAncestor = true
  569. break
  570. }
  571. parent = parent.parent
  572. }
  573. // no pending parent suspense nor transition, flush all jobs
  574. if (!hasUnresolvedAncestor && !delayEnter) {
  575. queuePostFlushCb(effects)
  576. }
  577. suspense.effects = []
  578. // resolve parent suspense if all async deps are resolved
  579. if (isSuspensible) {
  580. if (
  581. parentSuspense &&
  582. parentSuspense.pendingBranch &&
  583. parentSuspenseId === parentSuspense.pendingId
  584. ) {
  585. parentSuspense.deps--
  586. if (parentSuspense.deps === 0 && !sync) {
  587. parentSuspense.resolve()
  588. }
  589. }
  590. }
  591. // invoke @resolve event
  592. triggerEvent(vnode, 'onResolve')
  593. },
  594. fallback(fallbackVNode) {
  595. if (!suspense.pendingBranch) {
  596. return
  597. }
  598. const { vnode, activeBranch, parentComponent, container, namespace } =
  599. suspense
  600. // invoke @fallback event
  601. triggerEvent(vnode, 'onFallback')
  602. const anchor = next(activeBranch!)
  603. const mountFallback = () => {
  604. if (!suspense.isInFallback) {
  605. return
  606. }
  607. // mount the fallback tree
  608. patch(
  609. null,
  610. fallbackVNode,
  611. container,
  612. anchor,
  613. parentComponent,
  614. null, // fallback tree will not have suspense context
  615. namespace,
  616. slotScopeIds,
  617. optimized,
  618. )
  619. setActiveBranch(suspense, fallbackVNode)
  620. }
  621. const delayEnter =
  622. fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
  623. if (delayEnter) {
  624. activeBranch!.transition!.afterLeave = mountFallback
  625. }
  626. suspense.isInFallback = true
  627. // unmount current active branch
  628. unmount(
  629. activeBranch!,
  630. parentComponent,
  631. null, // no suspense so unmount hooks fire now
  632. true, // shouldRemove
  633. )
  634. if (!delayEnter) {
  635. mountFallback()
  636. }
  637. },
  638. move(container, anchor, type) {
  639. suspense.activeBranch &&
  640. move(suspense.activeBranch, container, anchor, type)
  641. suspense.container = container
  642. },
  643. next() {
  644. return suspense.activeBranch && next(suspense.activeBranch)
  645. },
  646. registerDep(instance, setupRenderEffect) {
  647. const isInPendingSuspense = !!suspense.pendingBranch
  648. if (isInPendingSuspense) {
  649. suspense.deps++
  650. }
  651. const hydratedEl = instance.vnode.el
  652. instance
  653. .asyncDep!.catch(err => {
  654. handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
  655. })
  656. .then(asyncSetupResult => {
  657. // retry when the setup() promise resolves.
  658. // component may have been unmounted before resolve.
  659. if (
  660. instance.isUnmounted ||
  661. suspense.isUnmounted ||
  662. suspense.pendingId !== instance.suspenseId
  663. ) {
  664. return
  665. }
  666. // retry from this component
  667. instance.asyncResolved = true
  668. const { vnode } = instance
  669. if (__DEV__) {
  670. pushWarningContext(vnode)
  671. }
  672. handleSetupResult(instance, asyncSetupResult, false)
  673. if (hydratedEl) {
  674. // vnode may have been replaced if an update happened before the
  675. // async dep is resolved.
  676. vnode.el = hydratedEl
  677. }
  678. const placeholder = !hydratedEl && instance.subTree.el
  679. setupRenderEffect(
  680. instance,
  681. vnode,
  682. // component may have been moved before resolve.
  683. // if this is not a hydration, instance.subTree will be the comment
  684. // placeholder.
  685. parentNode(hydratedEl || instance.subTree.el!)!,
  686. // anchor will not be used if this is hydration, so only need to
  687. // consider the comment placeholder case.
  688. hydratedEl ? null : next(instance.subTree),
  689. suspense,
  690. namespace,
  691. optimized,
  692. )
  693. if (placeholder) {
  694. remove(placeholder)
  695. }
  696. updateHOCHostEl(instance, vnode.el)
  697. if (__DEV__) {
  698. popWarningContext()
  699. }
  700. // only decrease deps count if suspense is not already resolved
  701. if (isInPendingSuspense && --suspense.deps === 0) {
  702. suspense.resolve()
  703. }
  704. })
  705. },
  706. unmount(parentSuspense, doRemove) {
  707. suspense.isUnmounted = true
  708. if (suspense.activeBranch) {
  709. unmount(
  710. suspense.activeBranch,
  711. parentComponent,
  712. parentSuspense,
  713. doRemove,
  714. )
  715. }
  716. if (suspense.pendingBranch) {
  717. unmount(
  718. suspense.pendingBranch,
  719. parentComponent,
  720. parentSuspense,
  721. doRemove,
  722. )
  723. }
  724. },
  725. }
  726. return suspense
  727. }
  728. function hydrateSuspense(
  729. node: Node,
  730. vnode: VNode,
  731. parentComponent: ComponentInternalInstance | null,
  732. parentSuspense: SuspenseBoundary | null,
  733. namespace: ElementNamespace,
  734. slotScopeIds: string[] | null,
  735. optimized: boolean,
  736. rendererInternals: RendererInternals,
  737. hydrateNode: (
  738. node: Node,
  739. vnode: VNode,
  740. parentComponent: ComponentInternalInstance | null,
  741. parentSuspense: SuspenseBoundary | null,
  742. slotScopeIds: string[] | null,
  743. optimized: boolean,
  744. ) => Node | null,
  745. ): Node | null {
  746. const suspense = (vnode.suspense = createSuspenseBoundary(
  747. vnode,
  748. parentSuspense,
  749. parentComponent,
  750. node.parentNode!,
  751. // eslint-disable-next-line no-restricted-globals
  752. document.createElement('div'),
  753. null,
  754. namespace,
  755. slotScopeIds,
  756. optimized,
  757. rendererInternals,
  758. true /* hydrating */,
  759. ))
  760. // there are two possible scenarios for server-rendered suspense:
  761. // - success: ssr content should be fully resolved
  762. // - failure: ssr content should be the fallback branch.
  763. // however, on the client we don't really know if it has failed or not
  764. // attempt to hydrate the DOM assuming it has succeeded, but we still
  765. // need to construct a suspense boundary first
  766. const result = hydrateNode(
  767. node,
  768. (suspense.pendingBranch = vnode.ssContent!),
  769. parentComponent,
  770. suspense,
  771. slotScopeIds,
  772. optimized,
  773. )
  774. if (suspense.deps === 0) {
  775. suspense.resolve(false, true)
  776. }
  777. return result
  778. /* eslint-enable no-restricted-globals */
  779. }
  780. function normalizeSuspenseChildren(vnode: VNode) {
  781. const { shapeFlag, children } = vnode
  782. const isSlotChildren = shapeFlag & ShapeFlags.SLOTS_CHILDREN
  783. vnode.ssContent = normalizeSuspenseSlot(
  784. isSlotChildren ? (children as Slots).default : children,
  785. )
  786. vnode.ssFallback = isSlotChildren
  787. ? normalizeSuspenseSlot((children as Slots).fallback)
  788. : createVNode(Comment)
  789. }
  790. function normalizeSuspenseSlot(s: any) {
  791. let block: VNode[] | null | undefined
  792. if (isFunction(s)) {
  793. const trackBlock = isBlockTreeEnabled && s._c
  794. if (trackBlock) {
  795. // disableTracking: false
  796. // allow block tracking for compiled slots
  797. // (see ./componentRenderContext.ts)
  798. s._d = false
  799. openBlock()
  800. }
  801. s = s()
  802. if (trackBlock) {
  803. s._d = true
  804. block = currentBlock
  805. closeBlock()
  806. }
  807. }
  808. if (isArray(s)) {
  809. const singleChild = filterSingleRoot(s)
  810. if (
  811. __DEV__ &&
  812. !singleChild &&
  813. s.filter(child => child !== NULL_DYNAMIC_COMPONENT).length > 0
  814. ) {
  815. warn(`<Suspense> slots expect a single root node.`)
  816. }
  817. s = singleChild
  818. }
  819. s = normalizeVNode(s)
  820. if (block && !s.dynamicChildren) {
  821. s.dynamicChildren = block.filter(c => c !== s)
  822. }
  823. return s
  824. }
  825. export function queueEffectWithSuspense(
  826. fn: Function | Function[],
  827. suspense: SuspenseBoundary | null,
  828. ): void {
  829. if (suspense && suspense.pendingBranch) {
  830. if (isArray(fn)) {
  831. suspense.effects.push(...fn)
  832. } else {
  833. suspense.effects.push(fn)
  834. }
  835. } else {
  836. queuePostFlushCb(fn)
  837. }
  838. }
  839. function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
  840. suspense.activeBranch = branch
  841. const { vnode, parentComponent } = suspense
  842. let el = branch.el
  843. // if branch has no el after patch, it's a HOC wrapping async components
  844. // drill and locate the placeholder comment node
  845. while (!el && branch.component) {
  846. branch = branch.component.subTree
  847. el = branch.el
  848. }
  849. vnode.el = el
  850. // in case suspense is the root node of a component,
  851. // recursively update the HOC el
  852. if (parentComponent && parentComponent.subTree === vnode) {
  853. parentComponent.vnode.el = el
  854. updateHOCHostEl(parentComponent, el)
  855. }
  856. }
  857. function isVNodeSuspensible(vnode: VNode) {
  858. return vnode.props?.suspensible != null && vnode.props.suspensible !== false
  859. }