componentSlots.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { type IfAny, isArray, isFunction } from '@vue/shared'
  2. import {
  3. type EffectScope,
  4. effectScope,
  5. isReactive,
  6. shallowReactive,
  7. } from '@vue/reactivity'
  8. import {
  9. type ComponentInternalInstance,
  10. currentInstance,
  11. setCurrentInstance,
  12. } from './component'
  13. import { type Block, type Fragment, fragmentKey } from './apiRender'
  14. import { firstEffect, renderEffect } from './renderEffect'
  15. import { createComment, createTextNode, insert, remove } from './dom/element'
  16. import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
  17. // TODO: SSR
  18. export type Slot<T extends any = any> = (
  19. ...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
  20. ) => Block
  21. export type InternalSlots = {
  22. [name: string]: Slot | undefined
  23. }
  24. export type Slots = Readonly<InternalSlots>
  25. export interface DynamicSlot {
  26. name: string
  27. fn: Slot
  28. key?: string
  29. }
  30. export type DynamicSlots = () => (DynamicSlot | DynamicSlot[])[]
  31. export function initSlots(
  32. instance: ComponentInternalInstance,
  33. rawSlots: InternalSlots | null = null,
  34. dynamicSlots: DynamicSlots | null = null,
  35. ) {
  36. let slots: InternalSlots = {}
  37. for (const key in rawSlots) {
  38. const slot = rawSlots[key]
  39. if (slot) {
  40. slots[key] = withCtx(slot)
  41. }
  42. }
  43. if (dynamicSlots) {
  44. slots = shallowReactive(slots)
  45. const dynamicSlotKeys: Record<string, true> = {}
  46. firstEffect(instance, () => {
  47. const _dynamicSlots: (DynamicSlot | DynamicSlot[])[] =
  48. callWithAsyncErrorHandling(
  49. dynamicSlots,
  50. instance,
  51. VaporErrorCodes.RENDER_FUNCTION,
  52. )
  53. for (let i = 0; i < _dynamicSlots.length; i++) {
  54. const slot = _dynamicSlots[i]
  55. // array of dynamic slot generated by <template v-for="..." #[...]>
  56. if (isArray(slot)) {
  57. for (let j = 0; j < slot.length; j++) {
  58. slots[slot[j].name] = withCtx(slot[j].fn)
  59. dynamicSlotKeys[slot[j].name] = true
  60. }
  61. } else if (slot) {
  62. // conditional single slot generated by <template v-if="..." #foo>
  63. slots[slot.name] = withCtx(
  64. slot.key
  65. ? (...args: any[]) => {
  66. const res = slot.fn(...args)
  67. // attach branch key so each conditional branch is considered a
  68. // different fragment
  69. if (res) (res as any).key = slot.key
  70. return res
  71. }
  72. : slot.fn,
  73. )
  74. dynamicSlotKeys[slot.name] = true
  75. }
  76. }
  77. // delete stale slots
  78. for (const key in dynamicSlotKeys) {
  79. if (
  80. !_dynamicSlots.some(slot =>
  81. slot && isArray(slot)
  82. ? slot.some(s => s.name === key)
  83. : slot.name === key,
  84. )
  85. ) {
  86. delete slots[key]
  87. }
  88. }
  89. })
  90. }
  91. instance.slots = slots
  92. function withCtx(fn: Slot): Slot {
  93. return (...args: any[]) => {
  94. const reset = setCurrentInstance(instance.parent!)
  95. try {
  96. return fn(...args)
  97. } finally {
  98. reset()
  99. }
  100. }
  101. }
  102. }
  103. export function createSlot(
  104. name: string | (() => string),
  105. binds?: Record<string, (() => unknown) | undefined>,
  106. fallback?: () => Block,
  107. ): Block {
  108. let block: Block | undefined
  109. let branch: Slot | undefined
  110. let oldBranch: Slot | undefined
  111. let parent: ParentNode | undefined | null
  112. let scope: EffectScope | undefined
  113. const isDynamicName = isFunction(name)
  114. const instance = currentInstance!
  115. const { slots } = instance
  116. // When not using dynamic slots, simplify the process to improve performance
  117. if (!isDynamicName && !isReactive(slots)) {
  118. if ((branch = slots[name] || fallback)) {
  119. return branch(binds)
  120. } else {
  121. return []
  122. }
  123. }
  124. const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
  125. const anchor = __DEV__ ? createComment('slot') : createTextNode()
  126. const fragment: Fragment = {
  127. nodes: [],
  128. anchor,
  129. [fragmentKey]: true,
  130. }
  131. // TODO lifecycle hooks
  132. renderEffect(() => {
  133. if ((branch = getSlot() || fallback) !== oldBranch) {
  134. parent ||= anchor.parentNode
  135. if (block) {
  136. scope!.stop()
  137. remove(block, parent!)
  138. }
  139. if ((oldBranch = branch)) {
  140. scope = effectScope()
  141. fragment.nodes = block = scope.run(() => branch!(binds))!
  142. parent && insert(block, parent, anchor)
  143. } else {
  144. scope = block = undefined
  145. fragment.nodes = []
  146. }
  147. }
  148. })
  149. return fragment
  150. }