apiCreateFor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import {
  2. EffectScope,
  3. type ShallowRef,
  4. isReactive,
  5. isShallow,
  6. pauseTracking,
  7. resetTracking,
  8. shallowReadArray,
  9. shallowRef,
  10. toReactive,
  11. } from '@vue/reactivity'
  12. import { getSequence, isArray, isObject, isString } from '@vue/shared'
  13. import { createComment, createTextNode } from './dom/node'
  14. import {
  15. type Block,
  16. VaporFragment,
  17. insert,
  18. remove as removeBlock,
  19. } from './block'
  20. import { warn } from '@vue/runtime-dom'
  21. import { currentInstance, isVaporComponent } from './component'
  22. import type { DynamicSlot } from './componentSlots'
  23. import { renderEffect } from './renderEffect'
  24. import { VaporVForFlags } from '../../shared/src/vaporFlags'
  25. class ForBlock extends VaporFragment {
  26. scope: EffectScope | undefined
  27. key: any
  28. itemRef: ShallowRef<any>
  29. keyRef: ShallowRef<any> | undefined
  30. indexRef: ShallowRef<number | undefined> | undefined
  31. constructor(
  32. nodes: Block,
  33. scope: EffectScope | undefined,
  34. item: ShallowRef<any>,
  35. key: ShallowRef<any> | undefined,
  36. index: ShallowRef<number | undefined> | undefined,
  37. renderKey: any,
  38. ) {
  39. super(nodes)
  40. this.scope = scope
  41. this.itemRef = item
  42. this.keyRef = key
  43. this.indexRef = index
  44. this.key = renderKey
  45. }
  46. }
  47. type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
  48. type ResolvedSource = {
  49. values: any[]
  50. needsWrap: boolean
  51. keys?: string[]
  52. }
  53. /*! #__NO_SIDE_EFFECTS__ */
  54. export const createFor = (
  55. src: () => Source,
  56. renderItem: (
  57. item: ShallowRef<any>,
  58. key: ShallowRef<any>,
  59. index: ShallowRef<number | undefined>,
  60. ) => Block,
  61. getKey?: (item: any, key: any, index?: number) => any,
  62. flags = 0,
  63. // hydrationNode?: Node,
  64. ): VaporFragment => {
  65. let isMounted = false
  66. let oldBlocks: ForBlock[] = []
  67. let newBlocks: ForBlock[]
  68. let parent: ParentNode | undefined | null
  69. const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
  70. const frag = new VaporFragment(oldBlocks)
  71. const instance = currentInstance!
  72. const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
  73. const isComponent = flags & VaporVForFlags.IS_COMPONENT
  74. if (__DEV__ && !instance) {
  75. warn('createFor() can only be used inside setup()')
  76. }
  77. const renderList = () => {
  78. const source = normalizeSource(src())
  79. const newLength = source.values.length
  80. const oldLength = oldBlocks.length
  81. newBlocks = new Array(newLength)
  82. pauseTracking()
  83. if (!isMounted) {
  84. isMounted = true
  85. for (let i = 0; i < newLength; i++) {
  86. mount(source, i)
  87. }
  88. } else {
  89. parent = parent || parentAnchor!.parentNode
  90. if (!oldLength) {
  91. // fast path for all new
  92. for (let i = 0; i < newLength; i++) {
  93. mount(source, i)
  94. }
  95. } else if (!newLength) {
  96. // fast path for clearing all
  97. const doRemove = !canUseFastRemove
  98. for (let i = 0; i < oldLength; i++) {
  99. unmount(oldBlocks[i], doRemove)
  100. }
  101. if (canUseFastRemove) {
  102. parent!.textContent = ''
  103. parent!.appendChild(parentAnchor)
  104. }
  105. } else if (!getKey) {
  106. // unkeyed fast path
  107. const commonLength = Math.min(newLength, oldLength)
  108. for (let i = 0; i < commonLength; i++) {
  109. update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
  110. }
  111. for (let i = oldLength; i < newLength; i++) {
  112. mount(source, i)
  113. }
  114. for (let i = newLength; i < oldLength; i++) {
  115. unmount(oldBlocks[i])
  116. }
  117. } else {
  118. let i = 0
  119. let e1 = oldLength - 1 // prev ending index
  120. let e2 = newLength - 1 // next ending index
  121. // 1. sync from start
  122. // (a b) c
  123. // (a b) d e
  124. while (i <= e1 && i <= e2) {
  125. if (tryPatchIndex(source, i)) {
  126. i++
  127. } else {
  128. break
  129. }
  130. }
  131. // 2. sync from end
  132. // a (b c)
  133. // d e (b c)
  134. while (i <= e1 && i <= e2) {
  135. if (tryPatchIndex(source, i)) {
  136. e1--
  137. e2--
  138. } else {
  139. break
  140. }
  141. }
  142. // 3. common sequence + mount
  143. // (a b)
  144. // (a b) c
  145. // i = 2, e1 = 1, e2 = 2
  146. // (a b)
  147. // c (a b)
  148. // i = 0, e1 = -1, e2 = 0
  149. if (i > e1) {
  150. if (i <= e2) {
  151. const nextPos = e2 + 1
  152. const anchor =
  153. nextPos < newLength
  154. ? normalizeAnchor(newBlocks[nextPos].nodes)
  155. : parentAnchor
  156. while (i <= e2) {
  157. mount(source, i, anchor)
  158. i++
  159. }
  160. }
  161. }
  162. // 4. common sequence + unmount
  163. // (a b) c
  164. // (a b)
  165. // i = 2, e1 = 2, e2 = 1
  166. // a (b c)
  167. // (b c)
  168. // i = 0, e1 = 0, e2 = -1
  169. else if (i > e2) {
  170. while (i <= e1) {
  171. unmount(oldBlocks[i])
  172. i++
  173. }
  174. }
  175. // 5. unknown sequence
  176. // [i ... e1 + 1]: a b [c d e] f g
  177. // [i ... e2 + 1]: a b [e d c h] f g
  178. // i = 2, e1 = 4, e2 = 5
  179. else {
  180. const s1 = i // prev starting index
  181. const s2 = i // next starting index
  182. // 5.1 build key:index map for newChildren
  183. const keyToNewIndexMap = new Map()
  184. for (i = s2; i <= e2; i++) {
  185. keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
  186. }
  187. // 5.2 loop through old children left to be patched and try to patch
  188. // matching nodes & remove nodes that are no longer present
  189. let j
  190. let patched = 0
  191. const toBePatched = e2 - s2 + 1
  192. let moved = false
  193. // used to track whether any node has moved
  194. let maxNewIndexSoFar = 0
  195. // works as Map<newIndex, oldIndex>
  196. // Note that oldIndex is offset by +1
  197. // and oldIndex = 0 is a special value indicating the new node has
  198. // no corresponding old node.
  199. // used for determining longest stable subsequence
  200. const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
  201. for (i = s1; i <= e1; i++) {
  202. const prevBlock = oldBlocks[i]
  203. if (patched >= toBePatched) {
  204. // all new children have been patched so this can only be a removal
  205. unmount(prevBlock)
  206. } else {
  207. const newIndex = keyToNewIndexMap.get(prevBlock.key)
  208. if (newIndex == null) {
  209. unmount(prevBlock)
  210. } else {
  211. newIndexToOldIndexMap[newIndex - s2] = i + 1
  212. if (newIndex >= maxNewIndexSoFar) {
  213. maxNewIndexSoFar = newIndex
  214. } else {
  215. moved = true
  216. }
  217. update(
  218. (newBlocks[newIndex] = prevBlock),
  219. ...getItem(source, newIndex),
  220. )
  221. patched++
  222. }
  223. }
  224. }
  225. // 5.3 move and mount
  226. // generate longest stable subsequence only when nodes have moved
  227. const increasingNewIndexSequence = moved
  228. ? getSequence(newIndexToOldIndexMap)
  229. : []
  230. j = increasingNewIndexSequence.length - 1
  231. // looping backwards so that we can use last patched node as anchor
  232. for (i = toBePatched - 1; i >= 0; i--) {
  233. const nextIndex = s2 + i
  234. const anchor =
  235. nextIndex + 1 < newLength
  236. ? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
  237. : parentAnchor
  238. if (newIndexToOldIndexMap[i] === 0) {
  239. // mount new
  240. mount(source, nextIndex, anchor)
  241. } else if (moved) {
  242. // move if:
  243. // There is no stable subsequence (e.g. a reverse)
  244. // OR current node is not among the stable sequence
  245. if (j < 0 || i !== increasingNewIndexSequence[j]) {
  246. insert(newBlocks[nextIndex].nodes, parent!, anchor)
  247. } else {
  248. j--
  249. }
  250. }
  251. }
  252. }
  253. }
  254. }
  255. frag.nodes = [(oldBlocks = newBlocks)]
  256. if (parentAnchor) {
  257. frag.nodes.push(parentAnchor)
  258. }
  259. resetTracking()
  260. }
  261. const needKey = renderItem.length > 1
  262. const needIndex = renderItem.length > 2
  263. const mount = (
  264. source: ResolvedSource,
  265. idx: number,
  266. anchor: Node | undefined = parentAnchor,
  267. ): ForBlock => {
  268. const [item, key, index] = getItem(source, idx)
  269. const itemRef = shallowRef(item)
  270. // avoid creating refs if the render fn doesn't need it
  271. const keyRef = needKey ? shallowRef(key) : undefined
  272. const indexRef = needIndex ? shallowRef(index) : undefined
  273. let nodes: Block
  274. let scope: EffectScope | undefined
  275. if (isComponent) {
  276. // component already has its own scope so no outer scope needed
  277. nodes = renderItem(itemRef, keyRef as any, indexRef as any)
  278. } else {
  279. scope = new EffectScope()
  280. nodes = scope.run(() =>
  281. renderItem(itemRef, keyRef as any, indexRef as any),
  282. )!
  283. }
  284. const block = (newBlocks[idx] = new ForBlock(
  285. nodes,
  286. scope,
  287. itemRef,
  288. keyRef,
  289. indexRef,
  290. getKey && getKey(item, key, index),
  291. ))
  292. if (parent) insert(block.nodes, parent, anchor)
  293. return block
  294. }
  295. const tryPatchIndex = (source: any, idx: number) => {
  296. const block = oldBlocks[idx]
  297. const [item, key, index] = getItem(source, idx)
  298. if (block.key === getKey!(item, key, index)) {
  299. update((newBlocks[idx] = block), item)
  300. return true
  301. }
  302. }
  303. const update = (
  304. { itemRef, keyRef, indexRef }: ForBlock,
  305. newItem: any,
  306. newKey?: any,
  307. newIndex?: any,
  308. ) => {
  309. if (newItem !== itemRef.value) {
  310. itemRef.value = newItem
  311. }
  312. if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
  313. keyRef.value = newKey
  314. }
  315. if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) {
  316. indexRef.value = newIndex
  317. }
  318. }
  319. const unmount = ({ nodes, scope }: ForBlock, doRemove = true) => {
  320. scope && scope.stop()
  321. doRemove && removeBlock(nodes, parent!)
  322. }
  323. if (flags & VaporVForFlags.ONCE) {
  324. renderList()
  325. } else {
  326. renderEffect(renderList)
  327. }
  328. return frag
  329. }
  330. export function createForSlots(
  331. rawSource: Source,
  332. getSlot: (item: any, key: any, index?: number) => DynamicSlot,
  333. ): DynamicSlot[] {
  334. const source = normalizeSource(rawSource)
  335. const sourceLength = source.values.length
  336. const slots = new Array<DynamicSlot>(sourceLength)
  337. for (let i = 0; i < sourceLength; i++) {
  338. slots[i] = getSlot(...getItem(source, i))
  339. }
  340. return slots
  341. }
  342. function normalizeSource(source: any): ResolvedSource {
  343. let values = source
  344. let needsWrap = false
  345. let keys
  346. if (isArray(source)) {
  347. if (isReactive(source)) {
  348. needsWrap = !isShallow(source)
  349. values = shallowReadArray(source)
  350. }
  351. } else if (isString(source)) {
  352. values = source.split('')
  353. } else if (typeof source === 'number') {
  354. if (__DEV__ && !Number.isInteger(source)) {
  355. warn(`The v-for range expect an integer value but got ${source}.`)
  356. }
  357. values = new Array(source)
  358. for (let i = 0; i < source; i++) values[i] = i + 1
  359. } else if (isObject(source)) {
  360. if (source[Symbol.iterator as any]) {
  361. values = Array.from(source as Iterable<any>)
  362. } else {
  363. keys = Object.keys(source)
  364. values = new Array(keys.length)
  365. for (let i = 0, l = keys.length; i < l; i++) {
  366. values[i] = source[keys[i]]
  367. }
  368. }
  369. }
  370. return { values, needsWrap, keys }
  371. }
  372. function getItem(
  373. { keys, values, needsWrap }: ResolvedSource,
  374. idx: number,
  375. ): [item: any, key: any, index?: number] {
  376. const value = needsWrap ? toReactive(values[idx]) : values[idx]
  377. if (keys) {
  378. return [value, keys[idx], idx]
  379. } else {
  380. return [value, idx, undefined]
  381. }
  382. }
  383. function normalizeAnchor(node: Block): Node {
  384. if (node instanceof Node) {
  385. return node
  386. } else if (isArray(node)) {
  387. return normalizeAnchor(node[0])
  388. } else if (isVaporComponent(node)) {
  389. return normalizeAnchor(node.block!)
  390. } else {
  391. return normalizeAnchor(node.nodes!)
  392. }
  393. }
  394. // runtime helper for rest element destructure
  395. export function getRestElement(val: any, keys: string[]): any {
  396. const res: any = {}
  397. for (const key in val) {
  398. if (!keys.includes(key)) res[key] = val[key]
  399. }
  400. return res
  401. }
  402. export function getDefaultValue(val: any, defaultVal: any): any {
  403. return val === undefined ? defaultVal : val
  404. }