apiDefineAsyncComponent.ts 6.1 KB

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