Suspense.ts 25 KB

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