block.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { isArray } from '@vue/shared'
  2. import {
  3. type VaporComponentInstance,
  4. isVaporComponent,
  5. mountComponent,
  6. unmountComponent,
  7. } from './component'
  8. import { createComment, createTextNode } from './dom/node'
  9. import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
  10. import { isHydrating } from './dom/hydration'
  11. export type Block =
  12. | Node
  13. | VaporFragment
  14. | DynamicFragment
  15. | VaporComponentInstance
  16. | Block[]
  17. export type BlockFn = (...args: any[]) => Block
  18. export class VaporFragment {
  19. nodes: Block
  20. target?: ParentNode | null
  21. targetAnchor?: Node | null
  22. anchor?: Node
  23. insert?: (parent: ParentNode, anchor: Node | null) => void
  24. remove?: (parent?: ParentNode) => void
  25. constructor(nodes: Block) {
  26. this.nodes = nodes
  27. }
  28. }
  29. export class DynamicFragment extends VaporFragment {
  30. anchor: Node
  31. scope: EffectScope | undefined
  32. current?: BlockFn
  33. fallback?: BlockFn
  34. constructor(anchorLabel?: string) {
  35. super([])
  36. this.anchor =
  37. __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
  38. }
  39. update(render?: BlockFn, key: any = render): void {
  40. if (key === this.current) {
  41. return
  42. }
  43. this.current = key
  44. pauseTracking()
  45. const parent = this.anchor.parentNode
  46. // teardown previous branch
  47. if (this.scope) {
  48. this.scope.stop()
  49. parent && remove(this.nodes, parent)
  50. }
  51. if (render) {
  52. this.scope = new EffectScope()
  53. this.nodes = this.scope.run(render) || []
  54. if (parent) insert(this.nodes, parent, this.anchor)
  55. } else {
  56. this.scope = undefined
  57. this.nodes = []
  58. }
  59. if (this.fallback && !isValidBlock(this.nodes)) {
  60. parent && remove(this.nodes, parent)
  61. this.nodes =
  62. (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
  63. []
  64. parent && insert(this.nodes, parent, this.anchor)
  65. }
  66. resetTracking()
  67. }
  68. }
  69. export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
  70. return val instanceof VaporFragment
  71. }
  72. export function isBlock(val: NonNullable<unknown>): val is Block {
  73. return (
  74. val instanceof Node ||
  75. isArray(val) ||
  76. isVaporComponent(val) ||
  77. isFragment(val)
  78. )
  79. }
  80. export function isValidBlock(block: Block): boolean {
  81. if (block instanceof Node) {
  82. return !(block instanceof Comment)
  83. } else if (isVaporComponent(block)) {
  84. return isValidBlock(block.block)
  85. } else if (isArray(block)) {
  86. return block.length > 0 && block.every(isValidBlock)
  87. } else {
  88. // fragment
  89. return isValidBlock(block.nodes)
  90. }
  91. }
  92. export function insert(
  93. block: Block,
  94. parent: ParentNode,
  95. anchor: Node | null | 0 = null, // 0 means prepend
  96. ): void {
  97. anchor = anchor === 0 ? parent.firstChild : anchor
  98. if (block instanceof Node) {
  99. if (!isHydrating) {
  100. parent.insertBefore(block, anchor)
  101. }
  102. } else if (isVaporComponent(block)) {
  103. if (block.isMounted) {
  104. insert(block.block!, parent, anchor)
  105. } else {
  106. mountComponent(block, parent, anchor)
  107. }
  108. } else if (isArray(block)) {
  109. for (const b of block) {
  110. insert(b, parent, anchor)
  111. }
  112. } else {
  113. // fragment
  114. if (block.insert) {
  115. // TODO handle hydration for vdom interop
  116. block.insert(parent, anchor)
  117. } else {
  118. insert(block.nodes, block.target || parent, block.targetAnchor || anchor)
  119. }
  120. if (block.anchor) insert(block.anchor, parent, anchor)
  121. }
  122. }
  123. export type InsertFn = typeof insert
  124. export function prepend(parent: ParentNode, ...blocks: Block[]): void {
  125. let i = blocks.length
  126. while (i--) insert(blocks[i], parent, 0)
  127. }
  128. export function remove(block: Block, parent?: ParentNode): void {
  129. if (block instanceof Node) {
  130. parent && parent.removeChild(block)
  131. } else if (isVaporComponent(block)) {
  132. unmountComponent(block, parent)
  133. } else if (isArray(block)) {
  134. for (let i = 0; i < block.length; i++) {
  135. remove(block[i], parent)
  136. }
  137. } else {
  138. // fragment
  139. if (block.remove) {
  140. block.remove(parent)
  141. } else {
  142. remove(block.nodes, parent)
  143. }
  144. if (block.anchor) remove(block.anchor, parent)
  145. if ((block as DynamicFragment).scope) {
  146. ;(block as DynamicFragment).scope!.stop()
  147. }
  148. }
  149. }
  150. /**
  151. * dev / test only
  152. */
  153. export function normalizeBlock(block: Block): Node[] {
  154. if (!__DEV__ && !__TEST__) {
  155. throw new Error(
  156. 'normalizeBlock should not be used in production code paths',
  157. )
  158. }
  159. const nodes: Node[] = []
  160. if (block instanceof Node) {
  161. nodes.push(block)
  162. } else if (isArray(block)) {
  163. block.forEach(child => nodes.push(...normalizeBlock(child)))
  164. } else if (isVaporComponent(block)) {
  165. nodes.push(...normalizeBlock(block.block!))
  166. } else {
  167. nodes.push(...normalizeBlock(block.nodes))
  168. block.anchor && nodes.push(block.anchor)
  169. }
  170. return nodes
  171. }