Suspense.ts 19 KB

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