block.ts 4.9 KB

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