apiCreateFor.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. import {
  2. EffectScope,
  3. type ShallowRef,
  4. isReactive,
  5. isReadonly,
  6. isShallow,
  7. setActiveSub,
  8. shallowReadArray,
  9. shallowRef,
  10. toReactive,
  11. toReadonly,
  12. watch,
  13. } from '@vue/reactivity'
  14. import { FOR_ANCHOR_LABEL, isArray, isObject, isString } from '@vue/shared'
  15. import { createComment, createTextNode } from './dom/node'
  16. import { type Block, insert, remove, remove as removeBlock } from './block'
  17. import { warn } from '@vue/runtime-dom'
  18. import { currentInstance, isVaporComponent } from './component'
  19. import type { DynamicSlot } from './componentSlots'
  20. import { renderEffect } from './renderEffect'
  21. import { VaporVForFlags } from '../../shared/src/vaporFlags'
  22. import {
  23. advanceHydrationNode,
  24. currentHydrationNode,
  25. isHydrating,
  26. locateHydrationNode,
  27. locateVaporFragmentAnchor,
  28. updateNextChildToHydrate,
  29. } from './dom/hydration'
  30. import { ForFragment, VaporFragment } from './fragment'
  31. import {
  32. insertionAnchor,
  33. insertionParent,
  34. resetInsertionState,
  35. } from './insertionState'
  36. import { applyTransitionHooks } from './components/Transition'
  37. class ForBlock extends VaporFragment {
  38. scope: EffectScope | undefined
  39. key: any
  40. itemRef: ShallowRef<any>
  41. keyRef: ShallowRef<any> | undefined
  42. indexRef: ShallowRef<number | undefined> | undefined
  43. constructor(
  44. nodes: Block,
  45. scope: EffectScope | undefined,
  46. item: ShallowRef<any>,
  47. key: ShallowRef<any> | undefined,
  48. index: ShallowRef<number | undefined> | undefined,
  49. renderKey: any,
  50. ) {
  51. super(nodes)
  52. this.scope = scope
  53. this.itemRef = item
  54. this.keyRef = key
  55. this.indexRef = index
  56. this.key = renderKey
  57. }
  58. }
  59. type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
  60. type ResolvedSource = {
  61. values: any[]
  62. needsWrap: boolean
  63. isReadonlySource: boolean
  64. keys?: string[]
  65. }
  66. export const createFor = (
  67. src: () => Source,
  68. renderItem: (
  69. item: ShallowRef<any>,
  70. key: ShallowRef<any>,
  71. index: ShallowRef<number | undefined>,
  72. ) => Block,
  73. getKey?: (item: any, key: any, index?: number) => any,
  74. flags = 0,
  75. setup?: (_: {
  76. createSelector: (source: () => any) => (cb: () => void) => void
  77. }) => void,
  78. ): ForFragment => {
  79. const _insertionParent = insertionParent
  80. const _insertionAnchor = insertionAnchor
  81. if (isHydrating) {
  82. locateHydrationNode()
  83. } else {
  84. resetInsertionState()
  85. }
  86. let isMounted = false
  87. let oldBlocks: ForBlock[] = []
  88. let newBlocks: ForBlock[]
  89. let parent: ParentNode | undefined | null
  90. // useSelector only
  91. let currentKey: any
  92. let parentAnchor: Node
  93. if (isHydrating) {
  94. parentAnchor = locateVaporFragmentAnchor(
  95. currentHydrationNode!,
  96. FOR_ANCHOR_LABEL,
  97. )!
  98. if (__DEV__ && !parentAnchor) {
  99. // this should not happen
  100. throw new Error(`v-for fragment anchor node was not found.`)
  101. }
  102. } else {
  103. parentAnchor = __DEV__ ? createComment('for') : createTextNode()
  104. }
  105. const frag = new ForFragment(oldBlocks)
  106. const instance = currentInstance!
  107. const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
  108. const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
  109. const selectors: {
  110. deregister: (key: any) => void
  111. cleanup: () => void
  112. }[] = []
  113. if (__DEV__ && !instance) {
  114. warn('createFor() can only be used inside setup()')
  115. }
  116. const renderList = () => {
  117. const source = normalizeSource(src())
  118. const newLength = source.values.length
  119. const oldLength = oldBlocks.length
  120. newBlocks = new Array(newLength)
  121. let isFallback = false
  122. const prevSub = setActiveSub()
  123. if (!isMounted) {
  124. isMounted = true
  125. for (let i = 0; i < newLength; i++) {
  126. // TODO add tests
  127. if (isHydrating && i > 0 && _insertionParent) {
  128. updateNextChildToHydrate(_insertionParent)
  129. }
  130. mount(source, i)
  131. }
  132. } else {
  133. parent = parent || parentAnchor!.parentNode
  134. if (!oldLength) {
  135. // remove fallback nodes
  136. if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) {
  137. remove(frag.nodes[0], parent!)
  138. }
  139. // fast path for all new
  140. for (let i = 0; i < newLength; i++) {
  141. mount(source, i)
  142. }
  143. } else if (!newLength) {
  144. // fast path for clearing all
  145. for (const selector of selectors) {
  146. selector.cleanup()
  147. }
  148. const doRemove = !canUseFastRemove
  149. for (let i = 0; i < oldLength; i++) {
  150. unmount(oldBlocks[i], doRemove, false)
  151. }
  152. if (canUseFastRemove) {
  153. parent!.textContent = ''
  154. parent!.appendChild(parentAnchor)
  155. }
  156. // render fallback nodes
  157. if (frag.fallback) {
  158. insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
  159. isFallback = true
  160. }
  161. } else if (!getKey) {
  162. // unkeyed fast path
  163. const commonLength = Math.min(newLength, oldLength)
  164. for (let i = 0; i < commonLength; i++) {
  165. update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
  166. }
  167. for (let i = oldLength; i < newLength; i++) {
  168. mount(source, i)
  169. }
  170. for (let i = newLength; i < oldLength; i++) {
  171. unmount(oldBlocks[i])
  172. }
  173. } else {
  174. if (__DEV__) {
  175. const keyToIndexMap: Map<any, number> = new Map()
  176. for (let i = 0; i < newLength; i++) {
  177. const item = getItem(source, i)
  178. const key = getKey(...item)
  179. if (key != null) {
  180. if (keyToIndexMap.has(key)) {
  181. warn(
  182. `Duplicate keys found during update:`,
  183. JSON.stringify(key),
  184. `Make sure keys are unique.`,
  185. )
  186. }
  187. keyToIndexMap.set(key, i)
  188. }
  189. }
  190. }
  191. const sharedBlockCount = Math.min(oldLength, newLength)
  192. const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
  193. const queuedBlocks: [
  194. blockIndex: number,
  195. blockItem: ReturnType<typeof getItem>,
  196. blockKey: any,
  197. ][] = new Array(newLength)
  198. let anchorFallback: Node = parentAnchor
  199. let endOffset = 0
  200. let startOffset = 0
  201. let queuedBlocksInsertIndex = 0
  202. let previousKeyIndexInsertIndex = 0
  203. while (endOffset < sharedBlockCount) {
  204. const currentIndex = newLength - endOffset - 1
  205. const currentItem = getItem(source, currentIndex)
  206. const currentKey = getKey(...currentItem)
  207. const existingBlock = oldBlocks[oldLength - endOffset - 1]
  208. if (existingBlock.key === currentKey) {
  209. update(existingBlock, ...currentItem)
  210. newBlocks[currentIndex] = existingBlock
  211. endOffset++
  212. continue
  213. }
  214. break
  215. }
  216. if (endOffset !== 0) {
  217. anchorFallback = normalizeAnchor(
  218. newBlocks[newLength - endOffset].nodes,
  219. )!
  220. }
  221. while (startOffset < sharedBlockCount - endOffset) {
  222. const currentItem = getItem(source, startOffset)
  223. const currentKey = getKey(...currentItem)
  224. const previousBlock = oldBlocks[startOffset]
  225. const previousKey = previousBlock.key
  226. if (previousKey === currentKey) {
  227. update((newBlocks[startOffset] = previousBlock), currentItem[0])
  228. } else {
  229. queuedBlocks[queuedBlocksInsertIndex++] = [
  230. startOffset,
  231. currentItem,
  232. currentKey,
  233. ]
  234. previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
  235. previousKey,
  236. startOffset,
  237. ]
  238. }
  239. startOffset++
  240. }
  241. for (let i = startOffset; i < oldLength - endOffset; i++) {
  242. previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
  243. oldBlocks[i].key,
  244. i,
  245. ]
  246. }
  247. const preparationBlockCount = Math.min(
  248. newLength - endOffset,
  249. sharedBlockCount,
  250. )
  251. for (let i = startOffset; i < preparationBlockCount; i++) {
  252. const blockItem = getItem(source, i)
  253. const blockKey = getKey(...blockItem)
  254. queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
  255. }
  256. if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) {
  257. for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
  258. const blockItem = getItem(source, i)
  259. const blockKey = getKey(...blockItem)
  260. mount(source, i, anchorFallback, blockItem, blockKey)
  261. }
  262. } else {
  263. queuedBlocks.length = queuedBlocksInsertIndex
  264. previousKeyIndexPairs.length = previousKeyIndexInsertIndex
  265. const previousKeyIndexMap = new Map(previousKeyIndexPairs)
  266. const operations: (() => void)[] = []
  267. let mountCounter = 0
  268. const relocateOrMountBlock = (
  269. blockIndex: number,
  270. blockItem: ReturnType<typeof getItem>,
  271. blockKey: any,
  272. anchorOffset: number,
  273. ) => {
  274. const previousIndex = previousKeyIndexMap.get(blockKey)
  275. if (previousIndex !== undefined) {
  276. const reusedBlock = (newBlocks[blockIndex] =
  277. oldBlocks[previousIndex])
  278. update(reusedBlock, ...blockItem)
  279. previousKeyIndexMap.delete(blockKey)
  280. if (previousIndex !== blockIndex) {
  281. operations.push(() =>
  282. insert(
  283. reusedBlock,
  284. parent!,
  285. anchorOffset === -1
  286. ? anchorFallback
  287. : normalizeAnchor(newBlocks[anchorOffset].nodes),
  288. ),
  289. )
  290. }
  291. } else {
  292. mountCounter++
  293. operations.push(() =>
  294. mount(
  295. source,
  296. blockIndex,
  297. anchorOffset === -1
  298. ? anchorFallback
  299. : normalizeAnchor(newBlocks[anchorOffset].nodes),
  300. blockItem,
  301. blockKey,
  302. ),
  303. )
  304. }
  305. }
  306. for (let i = queuedBlocks.length - 1; i >= 0; i--) {
  307. const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
  308. relocateOrMountBlock(
  309. blockIndex,
  310. blockItem,
  311. blockKey,
  312. blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
  313. )
  314. }
  315. for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
  316. const blockItem = getItem(source, i)
  317. const blockKey = getKey(...blockItem)
  318. relocateOrMountBlock(i, blockItem, blockKey, -1)
  319. }
  320. const useFastRemove = mountCounter === newLength
  321. for (const leftoverIndex of previousKeyIndexMap.values()) {
  322. unmount(
  323. oldBlocks[leftoverIndex],
  324. !(useFastRemove && canUseFastRemove),
  325. !useFastRemove,
  326. )
  327. }
  328. if (useFastRemove) {
  329. for (const selector of selectors) {
  330. selector.cleanup()
  331. }
  332. if (canUseFastRemove) {
  333. parent!.textContent = ''
  334. parent!.appendChild(parentAnchor)
  335. }
  336. }
  337. // perform mount and move operations
  338. for (const action of operations) {
  339. action()
  340. }
  341. }
  342. }
  343. }
  344. if (!isFallback) {
  345. frag.nodes = [(oldBlocks = newBlocks)]
  346. if (parentAnchor) frag.nodes.push(parentAnchor)
  347. } else {
  348. oldBlocks = []
  349. }
  350. setActiveSub(prevSub)
  351. }
  352. const needKey = renderItem.length > 1
  353. const needIndex = renderItem.length > 2
  354. const mount = (
  355. source: ResolvedSource,
  356. idx: number,
  357. anchor: Node | undefined = parentAnchor,
  358. [item, key, index] = getItem(source, idx),
  359. key2 = getKey && getKey(item, key, index),
  360. ): ForBlock => {
  361. const itemRef = shallowRef(item)
  362. // avoid creating refs if the render fn doesn't need it
  363. const keyRef = needKey ? shallowRef(key) : undefined
  364. const indexRef = needIndex ? shallowRef(index) : undefined
  365. currentKey = key2
  366. let nodes: Block
  367. let scope: EffectScope | undefined
  368. if (isComponent) {
  369. // component already has its own scope so no outer scope needed
  370. nodes = renderItem(itemRef, keyRef as any, indexRef as any)
  371. } else {
  372. scope = new EffectScope()
  373. nodes = scope.run(() =>
  374. renderItem(itemRef, keyRef as any, indexRef as any),
  375. )!
  376. }
  377. const block = (newBlocks[idx] = new ForBlock(
  378. nodes,
  379. scope,
  380. itemRef,
  381. keyRef,
  382. indexRef,
  383. key2,
  384. ))
  385. // apply transition for new nodes
  386. if (frag.$transition) {
  387. applyTransitionHooks(block.nodes, frag.$transition, false)
  388. }
  389. if (parent) insert(block.nodes, parent, anchor)
  390. return block
  391. }
  392. const update = (
  393. { itemRef, keyRef, indexRef }: ForBlock,
  394. newItem: any,
  395. newKey?: any,
  396. newIndex?: any,
  397. ) => {
  398. if (newItem !== itemRef.value) {
  399. itemRef.value = newItem
  400. }
  401. if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
  402. keyRef.value = newKey
  403. }
  404. if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) {
  405. indexRef.value = newIndex
  406. }
  407. }
  408. const unmount = (block: ForBlock, doRemove = true, doDeregister = true) => {
  409. if (!isComponent) {
  410. block.scope!.stop()
  411. }
  412. if (doRemove) {
  413. removeBlock(block.nodes, parent!)
  414. }
  415. if (doDeregister) {
  416. for (const selector of selectors) {
  417. selector.deregister(block.key)
  418. }
  419. }
  420. }
  421. if (setup) {
  422. setup({ createSelector })
  423. }
  424. if (flags & VaporVForFlags.ONCE) {
  425. renderList()
  426. } else {
  427. renderEffect(renderList)
  428. }
  429. if (!isHydrating && _insertionParent) {
  430. insert(frag, _insertionParent, _insertionAnchor)
  431. }
  432. if (isHydrating) {
  433. advanceHydrationNode(
  434. _insertionAnchor !== undefined ? _insertionParent! : parentAnchor,
  435. )
  436. }
  437. return frag
  438. function createSelector(source: () => any): (cb: () => void) => void {
  439. let operMap = new Map<any, (() => void)[]>()
  440. let activeKey = source()
  441. let activeOpers: (() => void)[] | undefined
  442. watch(source, newValue => {
  443. if (activeOpers !== undefined) {
  444. for (const oper of activeOpers) {
  445. oper()
  446. }
  447. }
  448. activeOpers = operMap.get(newValue)
  449. if (activeOpers !== undefined) {
  450. for (const oper of activeOpers) {
  451. oper()
  452. }
  453. }
  454. })
  455. selectors.push({ deregister, cleanup })
  456. return register
  457. function cleanup() {
  458. operMap = new Map()
  459. activeOpers = undefined
  460. }
  461. function register(oper: () => void) {
  462. oper()
  463. let opers = operMap.get(currentKey)
  464. if (opers !== undefined) {
  465. opers.push(oper)
  466. } else {
  467. opers = [oper]
  468. operMap.set(currentKey, opers)
  469. if (currentKey === activeKey) {
  470. activeOpers = opers
  471. }
  472. }
  473. }
  474. function deregister(key: any) {
  475. operMap.delete(key)
  476. if (key === activeKey) {
  477. activeOpers = undefined
  478. }
  479. }
  480. }
  481. }
  482. export function createForSlots(
  483. rawSource: Source,
  484. getSlot: (item: any, key: any, index?: number) => DynamicSlot,
  485. ): DynamicSlot[] {
  486. const source = normalizeSource(rawSource)
  487. const sourceLength = source.values.length
  488. const slots = new Array<DynamicSlot>(sourceLength)
  489. for (let i = 0; i < sourceLength; i++) {
  490. slots[i] = getSlot(...getItem(source, i))
  491. }
  492. return slots
  493. }
  494. function normalizeSource(source: any): ResolvedSource {
  495. let values = source
  496. let needsWrap = false
  497. let isReadonlySource = false
  498. let keys
  499. if (isArray(source)) {
  500. if (isReactive(source)) {
  501. needsWrap = !isShallow(source)
  502. values = shallowReadArray(source)
  503. isReadonlySource = isReadonly(source)
  504. }
  505. } else if (isString(source)) {
  506. values = source.split('')
  507. } else if (typeof source === 'number') {
  508. if (__DEV__ && !Number.isInteger(source)) {
  509. warn(`The v-for range expect an integer value but got ${source}.`)
  510. }
  511. values = new Array(source)
  512. for (let i = 0; i < source; i++) values[i] = i + 1
  513. } else if (isObject(source)) {
  514. if (source[Symbol.iterator as any]) {
  515. values = Array.from(source as Iterable<any>)
  516. } else {
  517. keys = Object.keys(source)
  518. values = new Array(keys.length)
  519. for (let i = 0, l = keys.length; i < l; i++) {
  520. values[i] = source[keys[i]]
  521. }
  522. }
  523. }
  524. return {
  525. values,
  526. needsWrap,
  527. isReadonlySource,
  528. keys,
  529. }
  530. }
  531. function getItem(
  532. { keys, values, needsWrap, isReadonlySource }: ResolvedSource,
  533. idx: number,
  534. ): [item: any, key: any, index?: number] {
  535. const value = needsWrap
  536. ? isReadonlySource
  537. ? toReadonly(toReactive(values[idx]))
  538. : toReactive(values[idx])
  539. : values[idx]
  540. if (keys) {
  541. return [value, keys[idx], idx]
  542. } else {
  543. return [value, idx, undefined]
  544. }
  545. }
  546. function normalizeAnchor(node: Block): Node | undefined {
  547. if (node && node instanceof Node) {
  548. return node
  549. } else if (isArray(node)) {
  550. return normalizeAnchor(node[0])
  551. } else if (isVaporComponent(node)) {
  552. return normalizeAnchor(node.block!)
  553. } else {
  554. return normalizeAnchor(node.nodes!)
  555. }
  556. }
  557. // runtime helper for rest element destructure
  558. export function getRestElement(val: any, keys: string[]): any {
  559. const res: any = {}
  560. for (const key in val) {
  561. if (!keys.includes(key)) res[key] = val[key]
  562. }
  563. return res
  564. }
  565. export function getDefaultValue(val: any, defaultVal: any): any {
  566. return val === undefined ? defaultVal : val
  567. }
  568. export function isForBlock(block: Block): block is ForBlock {
  569. return block instanceof ForBlock
  570. }