componentSlots.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 = callWithAsyncErrorHandling(
  48. dynamicSlots,
  49. instance,
  50. VaporErrorCodes.RENDER_FUNCTION,
  51. )
  52. for (let i = 0; i < _dynamicSlots.length; i++) {
  53. const slot = _dynamicSlots[i]
  54. // array of dynamic slot generated by <template v-for="..." #[...]>
  55. if (isArray(slot)) {
  56. for (let j = 0; j < slot.length; j++) {
  57. slots[slot[j].name] = withCtx(slot[j].fn)
  58. dynamicSlotKeys[slot[j].name] = true
  59. }
  60. } else if (slot) {
  61. // conditional single slot generated by <template v-if="..." #foo>
  62. slots[slot.name] = withCtx(
  63. slot.key
  64. ? (...args: any[]) => {
  65. const res = slot.fn(...args)
  66. // attach branch key so each conditional branch is considered a
  67. // different fragment
  68. if (res) (res as any).key = slot.key
  69. return res
  70. }
  71. : slot.fn,
  72. )
  73. dynamicSlotKeys[slot.name] = true
  74. }
  75. }
  76. // delete stale slots
  77. for (const key in dynamicSlotKeys) {
  78. if (
  79. !_dynamicSlots.some(slot =>
  80. slot && isArray(slot)
  81. ? slot.some(s => s.name === key)
  82. : slot.name === key,
  83. )
  84. ) {
  85. delete slots[key]
  86. }
  87. }
  88. })
  89. }
  90. instance.slots = slots
  91. function withCtx(fn: Slot): Slot {
  92. return (...args: any[]) => {
  93. const reset = setCurrentInstance(instance.parent!)
  94. try {
  95. return fn(...args)
  96. } finally {
  97. reset()
  98. }
  99. }
  100. }
  101. }
  102. export function createSlot(
  103. name: string | (() => string),
  104. binds?: Record<string, (() => unknown) | undefined>,
  105. fallback?: () => Block,
  106. ): Block {
  107. let block: Block | undefined
  108. let branch: Slot | undefined
  109. let oldBranch: Slot | undefined
  110. let parent: ParentNode | undefined | null
  111. let scope: EffectScope | undefined
  112. const isDynamicName = isFunction(name)
  113. const instance = currentInstance!
  114. const { slots } = instance
  115. // When not using dynamic slots, simplify the process to improve performance
  116. if (!isDynamicName && !isReactive(slots)) {
  117. if ((branch = slots[name] || fallback)) {
  118. return branch(binds)
  119. } else {
  120. return []
  121. }
  122. }
  123. const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
  124. const anchor = __DEV__ ? createComment('slot') : createTextNode()
  125. const fragment: Fragment = {
  126. nodes: [],
  127. anchor,
  128. [fragmentKey]: true,
  129. }
  130. // TODO lifecycle hooks
  131. renderEffect(() => {
  132. if ((branch = getSlot() || fallback) !== oldBranch) {
  133. parent ||= anchor.parentNode
  134. if (block) {
  135. scope!.stop()
  136. remove(block, parent!)
  137. }
  138. if ((oldBranch = branch)) {
  139. scope = effectScope()
  140. fragment.nodes = block = scope.run(() => branch!(binds))!
  141. parent && insert(block, parent, anchor)
  142. } else {
  143. scope = block = undefined
  144. fragment.nodes = []
  145. }
  146. }
  147. })
  148. return fragment
  149. }