fragment.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import { EffectScope, setActiveSub } from '@vue/reactivity'
  2. import { createComment, createTextNode } from './dom/node'
  3. import {
  4. type Block,
  5. type BlockFn,
  6. type TransitionOptions,
  7. type VaporTransitionHooks,
  8. insert,
  9. isValidBlock,
  10. remove,
  11. } from './block'
  12. import type { TransitionHooks } from '@vue/runtime-dom'
  13. import {
  14. advanceHydrationNode,
  15. currentHydrationNode,
  16. isComment,
  17. isHydrating,
  18. locateHydrationNode,
  19. locateVaporFragmentAnchor,
  20. } from './dom/hydration'
  21. import {
  22. applyTransitionHooks,
  23. applyTransitionLeaveHooks,
  24. } from './components/Transition'
  25. import type { VaporComponentInstance } from './component'
  26. export class VaporFragment<T extends Block = Block>
  27. implements TransitionOptions
  28. {
  29. $key?: any
  30. $transition?: VaporTransitionHooks | undefined
  31. nodes: T
  32. anchor?: Node
  33. insert?: (
  34. parent: ParentNode,
  35. anchor: Node | null,
  36. transitionHooks?: TransitionHooks,
  37. ) => void
  38. remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
  39. fallback?: BlockFn
  40. target?: ParentNode | null
  41. targetAnchor?: Node | null
  42. getNodes?: () => Block
  43. setRef?: (comp: VaporComponentInstance) => void
  44. constructor(nodes: T) {
  45. this.nodes = nodes
  46. }
  47. }
  48. export class DynamicFragment extends VaporFragment {
  49. anchor!: Node
  50. scope: EffectScope | undefined
  51. current?: BlockFn
  52. fallback?: BlockFn
  53. /**
  54. * slot only
  55. * indicates forwarded slot
  56. */
  57. forwarded?: boolean
  58. teardown?: () => void
  59. anchorLabel?: string
  60. constructor(anchorLabel?: string) {
  61. super([])
  62. if (isHydrating) {
  63. locateHydrationNode(anchorLabel === 'slot')
  64. this.anchorLabel = anchorLabel
  65. } else {
  66. this.anchor =
  67. __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
  68. }
  69. }
  70. update(render?: BlockFn, key: any = render): void {
  71. if (key === this.current) {
  72. if (isHydrating) this.hydrate(this.anchorLabel!)
  73. return
  74. }
  75. this.current = key
  76. const prevSub = setActiveSub()
  77. const parent = isHydrating ? null : this.anchor.parentNode
  78. const transition = this.$transition
  79. const renderBranch = () => {
  80. if (render) {
  81. this.scope = new EffectScope()
  82. this.nodes = this.scope.run(render) || []
  83. if (transition) {
  84. this.$transition = applyTransitionHooks(this.nodes, transition)
  85. }
  86. if (parent) insert(this.nodes, parent, this.anchor)
  87. } else {
  88. this.scope = undefined
  89. this.nodes = []
  90. }
  91. }
  92. // teardown previous branch
  93. if (this.scope) {
  94. this.scope.stop()
  95. if (parent) this.teardown && this.teardown()
  96. const mode = transition && transition.mode
  97. if (mode) {
  98. applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
  99. parent && remove(this.nodes, parent)
  100. if (mode === 'out-in') {
  101. setActiveSub(prevSub)
  102. return
  103. }
  104. } else {
  105. parent && remove(this.nodes, parent)
  106. }
  107. }
  108. renderBranch()
  109. if (this.fallback) {
  110. // set fallback for nested fragments
  111. const hasNestedFragment = isFragment(this.nodes)
  112. if (hasNestedFragment) {
  113. setFragmentFallback(this.nodes as VaporFragment, this.fallback)
  114. }
  115. const invalidFragment = findInvalidFragment(this)
  116. if (invalidFragment) {
  117. parent && remove(this.nodes, parent)
  118. const scope = this.scope || (this.scope = new EffectScope())
  119. scope.run(() => {
  120. // for nested fragments, render invalid fragment's fallback
  121. if (hasNestedFragment) {
  122. renderFragmentFallback(invalidFragment)
  123. } else {
  124. this.nodes = this.fallback!() || []
  125. }
  126. })
  127. parent && insert(this.nodes, parent, this.anchor)
  128. }
  129. }
  130. setActiveSub(prevSub)
  131. if (isHydrating) this.hydrate(this.anchorLabel!)
  132. }
  133. hydrate(label: string): void {
  134. // for `v-if="false"` the node will be an empty comment, use it as the anchor.
  135. // otherwise, find next sibling vapor fragment anchor
  136. if (label === 'if' && isComment(currentHydrationNode!, '')) {
  137. this.anchor = currentHydrationNode
  138. } else {
  139. let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
  140. if (!anchor && (label === 'slot' || label === 'if')) {
  141. // fallback to fragment end anchor for ssr vdom slot
  142. anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
  143. }
  144. if (anchor) {
  145. this.anchor = anchor
  146. } else if (__DEV__) {
  147. // this should not happen
  148. throw new Error(`${label} fragment anchor node was not found.`)
  149. }
  150. }
  151. advanceHydrationNode(this.anchor)
  152. }
  153. }
  154. export class ForFragment extends VaporFragment<Block[]> {
  155. constructor(nodes: Block[]) {
  156. super(nodes)
  157. }
  158. }
  159. export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
  160. return val instanceof VaporFragment
  161. }
  162. export function setFragmentFallback(
  163. fragment: VaporFragment,
  164. fallback: BlockFn,
  165. ): void {
  166. // stop recursion if fragment has its own fallback
  167. if (fragment.fallback) return
  168. fragment.fallback = fallback
  169. if (isFragment(fragment.nodes)) {
  170. setFragmentFallback(fragment.nodes, fallback)
  171. }
  172. }
  173. function renderFragmentFallback(fragment: VaporFragment): void {
  174. if (fragment instanceof ForFragment) {
  175. fragment.nodes[0] = [fragment.fallback!() || []] as Block[]
  176. } else if (fragment instanceof DynamicFragment) {
  177. fragment.update(fragment.fallback)
  178. } else {
  179. // vdom slots
  180. }
  181. }
  182. function findInvalidFragment(fragment: VaporFragment): VaporFragment | null {
  183. if (isValidBlock(fragment.nodes)) return null
  184. return isFragment(fragment.nodes)
  185. ? findInvalidFragment(fragment.nodes) || fragment
  186. : fragment
  187. }