apiCreateFor.ts 11 KB

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