apiDefineAsyncComponent.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import {
  2. type AsyncComponentLoader,
  3. type AsyncComponentOptions,
  4. ErrorCodes,
  5. createAsyncComponentContext,
  6. currentInstance,
  7. handleError,
  8. markAsyncBoundary,
  9. performAsyncHydrate,
  10. setCurrentInstance,
  11. useAsyncComponentState,
  12. } from '@vue/runtime-dom'
  13. import { defineVaporComponent } from './apiDefineComponent'
  14. import {
  15. type VaporComponent,
  16. type VaporComponentInstance,
  17. createComponent,
  18. } from './component'
  19. import { renderEffect } from './renderEffect'
  20. import { DynamicFragment } from './fragment'
  21. import {
  22. hydrateNode,
  23. isComment,
  24. isHydrating,
  25. locateEndAnchor,
  26. setCurrentHydrationNode,
  27. } from './dom/hydration'
  28. import type { TransitionOptions } from './block'
  29. import { _next } from './dom/node'
  30. import { isKeepAliveEnabled } from './keepAlive'
  31. /*@ __NO_SIDE_EFFECTS__ */
  32. export function defineVaporAsyncComponent<T extends VaporComponent>(
  33. source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
  34. ): T {
  35. const {
  36. load,
  37. getResolvedComp,
  38. setPendingRequest,
  39. source: {
  40. loadingComponent,
  41. errorComponent,
  42. delay,
  43. hydrate: hydrateStrategy,
  44. timeout,
  45. suspensible = true,
  46. },
  47. } = createAsyncComponentContext<T, VaporComponent>(source)
  48. return defineVaporComponent({
  49. name: 'VaporAsyncComponentWrapper',
  50. __asyncLoader: load,
  51. __asyncHydrate(
  52. el: Element,
  53. instance: VaporComponentInstance,
  54. // Note: this hydrate function essentially calls the setup method of the component
  55. // not the actual hydrate function
  56. hydrate: () => void,
  57. ) {
  58. // early return allows tree-shaking of hydration logic when not used
  59. if (!isHydrating) return
  60. // Create placeholder block that matches the adopted DOM.
  61. // The async component may get unmounted before its inner component is loaded,
  62. // so we need to give it a placeholder block.
  63. if (isComment(el, '[')) {
  64. const end = _next(locateEndAnchor(el)!)
  65. const block = (instance.block = [el as Node])
  66. let cur = el as Node
  67. while (true) {
  68. let n = _next(cur)
  69. if (n && n !== end) {
  70. block.push((cur = n))
  71. } else {
  72. break
  73. }
  74. }
  75. } else {
  76. instance.block = el
  77. }
  78. // Mark as mounted to ensure it can be unmounted before
  79. // its inner component is resolved
  80. instance.isMounted = true
  81. // Advance current hydration node to the nextSibling
  82. setCurrentHydrationNode(
  83. isComment(el, '[') ? locateEndAnchor(el)! : el.nextSibling,
  84. )
  85. performAsyncHydrate(
  86. el,
  87. instance,
  88. () => hydrateNode(el, hydrate),
  89. getResolvedComp,
  90. load,
  91. hydrateStrategy,
  92. )
  93. },
  94. get __asyncResolved() {
  95. return getResolvedComp()
  96. },
  97. setup() {
  98. const instance = currentInstance as VaporComponentInstance &
  99. TransitionOptions
  100. markAsyncBoundary(instance)
  101. const frag =
  102. __DEV__ || isHydrating
  103. ? new DynamicFragment('async component')
  104. : new DynamicFragment()
  105. // already resolved
  106. let resolvedComp = getResolvedComp()
  107. if (resolvedComp) {
  108. frag!.update(() => createInnerComp(resolvedComp!, instance))
  109. return frag
  110. }
  111. const onError = (err: Error) => {
  112. setPendingRequest(null)
  113. handleError(
  114. err,
  115. instance,
  116. ErrorCodes.ASYNC_COMPONENT_LOADER,
  117. !errorComponent /* do not throw in dev if user provided error component */,
  118. )
  119. }
  120. if (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) {
  121. return load()
  122. .then(() => {
  123. resolvedComp = getResolvedComp()
  124. if (resolvedComp) {
  125. frag.update(() => createInnerComp(resolvedComp!, instance))
  126. }
  127. return frag
  128. })
  129. .catch(err => {
  130. onError(err)
  131. if (errorComponent) {
  132. frag.update(() =>
  133. createInnerComp(
  134. errorComponent,
  135. instance,
  136. { error: () => err },
  137. // Avoid wrapper slot fallthrough
  138. {},
  139. ),
  140. )
  141. }
  142. return frag
  143. })
  144. }
  145. const { loaded, error, delayed } = useAsyncComponentState(
  146. delay,
  147. timeout,
  148. onError,
  149. )
  150. load()
  151. .then(() => {
  152. loaded.value = true
  153. })
  154. .catch(err => {
  155. onError(err)
  156. error.value = err
  157. })
  158. renderEffect(() => {
  159. resolvedComp = getResolvedComp()
  160. let render
  161. if (loaded.value && resolvedComp) {
  162. render = () => createInnerComp(resolvedComp!, instance)
  163. } else if (error.value && errorComponent) {
  164. render = () =>
  165. createComponent(errorComponent, { error: () => error.value })
  166. } else if (loadingComponent && !delayed.value) {
  167. render = () => createComponent(loadingComponent)
  168. }
  169. frag.update(render)
  170. // Manually trigger cacheBlock for KeepAlive
  171. if (isKeepAliveEnabled && frag.keepAliveCtx) {
  172. frag.keepAliveCtx.cacheBlock()
  173. }
  174. })
  175. return frag
  176. },
  177. }) as T
  178. }
  179. function createInnerComp(
  180. comp: VaporComponent,
  181. parent: VaporComponentInstance & TransitionOptions,
  182. rawProps = parent.rawProps,
  183. rawSlots = parent.rawSlots,
  184. ): VaporComponentInstance {
  185. const prevInstance = setCurrentInstance(parent)
  186. try {
  187. return createComponent(
  188. comp,
  189. rawProps,
  190. rawSlots,
  191. // rawProps is shared and already contains fallthrough attrs.
  192. // so isSingleRoot should be undefined
  193. undefined,
  194. undefined,
  195. parent.appContext,
  196. )
  197. } finally {
  198. setCurrentInstance(...prevInstance)
  199. }
  200. }