|
|
@@ -11,7 +11,7 @@ import {
|
|
|
toReadonly,
|
|
|
watch,
|
|
|
} from '@vue/reactivity'
|
|
|
-import { getSequence, isArray, isObject, isString } from '@vue/shared'
|
|
|
+import { isArray, isObject, isString } from '@vue/shared'
|
|
|
import { createComment, createTextNode } from './dom/node'
|
|
|
import {
|
|
|
type Block,
|
|
|
@@ -150,149 +150,173 @@ export const createFor = (
|
|
|
unmount(oldBlocks[i])
|
|
|
}
|
|
|
} else {
|
|
|
- let i = 0
|
|
|
- let e1 = oldLength - 1 // prev ending index
|
|
|
- let e2 = newLength - 1 // next ending index
|
|
|
-
|
|
|
- // 1. sync from start
|
|
|
- // (a b) c
|
|
|
- // (a b) d e
|
|
|
- while (i <= e1 && i <= e2) {
|
|
|
- if (tryPatchIndex(source, i)) {
|
|
|
- i++
|
|
|
- } else {
|
|
|
- break
|
|
|
+ const sharedBlockCount = Math.min(oldLength, newLength)
|
|
|
+ const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
|
|
|
+ const queuedBlocks: [
|
|
|
+ blockIndex: number,
|
|
|
+ blockItem: ReturnType<typeof getItem>,
|
|
|
+ blockKey: any,
|
|
|
+ ][] = new Array(newLength)
|
|
|
+
|
|
|
+ let anchorFallback: Node = parentAnchor
|
|
|
+ let endOffset = 0
|
|
|
+ let startOffset = 0
|
|
|
+ let queuedBlocksInsertIndex = 0
|
|
|
+ let previousKeyIndexInsertIndex = 0
|
|
|
+
|
|
|
+ while (endOffset < sharedBlockCount) {
|
|
|
+ const currentIndex = newLength - endOffset - 1
|
|
|
+ const currentItem = getItem(source, currentIndex)
|
|
|
+ const currentKey = getKey(...currentItem)
|
|
|
+ const existingBlock = oldBlocks[oldLength - endOffset - 1]
|
|
|
+ if (existingBlock.key === currentKey) {
|
|
|
+ update(existingBlock, ...currentItem)
|
|
|
+ newBlocks[currentIndex] = existingBlock
|
|
|
+ endOffset++
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (endOffset !== 0) {
|
|
|
+ anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
|
|
|
}
|
|
|
+ break
|
|
|
}
|
|
|
|
|
|
- // 2. sync from end
|
|
|
- // a (b c)
|
|
|
- // d e (b c)
|
|
|
- while (i <= e1 && i <= e2) {
|
|
|
- if (tryPatchIndex(source, i)) {
|
|
|
- e1--
|
|
|
- e2--
|
|
|
+ while (startOffset < sharedBlockCount - endOffset) {
|
|
|
+ const currentItem = getItem(source, startOffset)
|
|
|
+ const currentKey = getKey(...currentItem)
|
|
|
+ const previousBlock = oldBlocks[startOffset]
|
|
|
+ const previousKey = previousBlock.key
|
|
|
+ if (previousKey === currentKey) {
|
|
|
+ update((newBlocks[startOffset] = previousBlock), currentItem[0])
|
|
|
} else {
|
|
|
- break
|
|
|
+ queuedBlocks[queuedBlocksInsertIndex++] = [
|
|
|
+ startOffset,
|
|
|
+ currentItem,
|
|
|
+ currentKey,
|
|
|
+ ]
|
|
|
+ previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
|
|
|
+ previousKey,
|
|
|
+ startOffset,
|
|
|
+ ]
|
|
|
}
|
|
|
+ startOffset++
|
|
|
}
|
|
|
|
|
|
- // 3. common sequence + mount
|
|
|
- // (a b)
|
|
|
- // (a b) c
|
|
|
- // i = 2, e1 = 1, e2 = 2
|
|
|
- // (a b)
|
|
|
- // c (a b)
|
|
|
- // i = 0, e1 = -1, e2 = 0
|
|
|
- if (i > e1) {
|
|
|
- if (i <= e2) {
|
|
|
- const nextPos = e2 + 1
|
|
|
- const anchor =
|
|
|
- nextPos < newLength
|
|
|
- ? normalizeAnchor(newBlocks[nextPos].nodes)
|
|
|
- : parentAnchor
|
|
|
- while (i <= e2) {
|
|
|
- mount(source, i, anchor)
|
|
|
- i++
|
|
|
- }
|
|
|
- }
|
|
|
+ for (let i = startOffset; i < oldLength - endOffset; i++) {
|
|
|
+ previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
|
|
|
+ oldBlocks[i].key,
|
|
|
+ i,
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
- // 4. common sequence + unmount
|
|
|
- // (a b) c
|
|
|
- // (a b)
|
|
|
- // i = 2, e1 = 2, e2 = 1
|
|
|
- // a (b c)
|
|
|
- // (b c)
|
|
|
- // i = 0, e1 = 0, e2 = -1
|
|
|
- else if (i > e2) {
|
|
|
- while (i <= e1) {
|
|
|
- unmount(oldBlocks[i])
|
|
|
- i++
|
|
|
- }
|
|
|
+ const preparationBlockCount = Math.min(
|
|
|
+ newLength - endOffset,
|
|
|
+ sharedBlockCount,
|
|
|
+ )
|
|
|
+ for (let i = startOffset; i < preparationBlockCount; i++) {
|
|
|
+ const blockItem = getItem(source, i)
|
|
|
+ const blockKey = getKey(...blockItem)
|
|
|
+ queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
|
|
|
}
|
|
|
|
|
|
- // 5. unknown sequence
|
|
|
- // [i ... e1 + 1]: a b [c d e] f g
|
|
|
- // [i ... e2 + 1]: a b [e d c h] f g
|
|
|
- // i = 2, e1 = 4, e2 = 5
|
|
|
- else {
|
|
|
- const s1 = i // prev starting index
|
|
|
- const s2 = i // next starting index
|
|
|
-
|
|
|
- // 5.1 build key:index map for newChildren
|
|
|
- const keyToNewIndexMap = new Map()
|
|
|
- for (i = s2; i <= e2; i++) {
|
|
|
- keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
|
|
|
+ if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) {
|
|
|
+ for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
|
|
|
+ const blockItem = getItem(source, i)
|
|
|
+ const blockKey = getKey(...blockItem)
|
|
|
+ mount(source, i, anchorFallback, blockItem, blockKey)
|
|
|
}
|
|
|
-
|
|
|
- // 5.2 loop through old children left to be patched and try to patch
|
|
|
- // matching nodes & remove nodes that are no longer present
|
|
|
- let j
|
|
|
- let patched = 0
|
|
|
- const toBePatched = e2 - s2 + 1
|
|
|
- let moved = false
|
|
|
- // used to track whether any node has moved
|
|
|
- let maxNewIndexSoFar = 0
|
|
|
- // works as Map<newIndex, oldIndex>
|
|
|
- // Note that oldIndex is offset by +1
|
|
|
- // and oldIndex = 0 is a special value indicating the new node has
|
|
|
- // no corresponding old node.
|
|
|
- // used for determining longest stable subsequence
|
|
|
- const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
|
|
|
-
|
|
|
- for (i = s1; i <= e1; i++) {
|
|
|
- const prevBlock = oldBlocks[i]
|
|
|
- if (patched >= toBePatched) {
|
|
|
- // all new children have been patched so this can only be a removal
|
|
|
- unmount(prevBlock)
|
|
|
+ } else {
|
|
|
+ queuedBlocks.length = queuedBlocksInsertIndex
|
|
|
+ previousKeyIndexPairs.length = previousKeyIndexInsertIndex
|
|
|
+
|
|
|
+ const previousKeyIndexMap = new Map(previousKeyIndexPairs)
|
|
|
+ const blocksToMount: [
|
|
|
+ blockIndex: number,
|
|
|
+ blockItem: ReturnType<typeof getItem>,
|
|
|
+ blockKey: any,
|
|
|
+ anchorOffset: number,
|
|
|
+ ][] = []
|
|
|
+
|
|
|
+ const relocateOrMountBlock = (
|
|
|
+ blockIndex: number,
|
|
|
+ blockItem: ReturnType<typeof getItem>,
|
|
|
+ blockKey: any,
|
|
|
+ anchorOffset: number,
|
|
|
+ ) => {
|
|
|
+ const previousIndex = previousKeyIndexMap.get(blockKey)
|
|
|
+ if (previousIndex !== undefined) {
|
|
|
+ const reusedBlock = (newBlocks[blockIndex] =
|
|
|
+ oldBlocks[previousIndex])
|
|
|
+ update(reusedBlock, ...blockItem)
|
|
|
+ insert(
|
|
|
+ reusedBlock,
|
|
|
+ parent!,
|
|
|
+ anchorOffset === -1
|
|
|
+ ? anchorFallback
|
|
|
+ : normalizeAnchor(newBlocks[anchorOffset].nodes),
|
|
|
+ )
|
|
|
+ previousKeyIndexMap.delete(blockKey)
|
|
|
} else {
|
|
|
- const newIndex = keyToNewIndexMap.get(prevBlock.key)
|
|
|
- if (newIndex == null) {
|
|
|
- unmount(prevBlock)
|
|
|
- } else {
|
|
|
- newIndexToOldIndexMap[newIndex - s2] = i + 1
|
|
|
- if (newIndex >= maxNewIndexSoFar) {
|
|
|
- maxNewIndexSoFar = newIndex
|
|
|
- } else {
|
|
|
- moved = true
|
|
|
- }
|
|
|
- update(
|
|
|
- (newBlocks[newIndex] = prevBlock),
|
|
|
- ...getItem(source, newIndex),
|
|
|
- )
|
|
|
- patched++
|
|
|
- }
|
|
|
+ blocksToMount.push([
|
|
|
+ blockIndex,
|
|
|
+ blockItem,
|
|
|
+ blockKey,
|
|
|
+ anchorOffset,
|
|
|
+ ])
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 5.3 move and mount
|
|
|
- // generate longest stable subsequence only when nodes have moved
|
|
|
- const increasingNewIndexSequence = moved
|
|
|
- ? getSequence(newIndexToOldIndexMap)
|
|
|
- : []
|
|
|
- j = increasingNewIndexSequence.length - 1
|
|
|
- // looping backwards so that we can use last patched node as anchor
|
|
|
- for (i = toBePatched - 1; i >= 0; i--) {
|
|
|
- const nextIndex = s2 + i
|
|
|
- const anchor =
|
|
|
- nextIndex + 1 < newLength
|
|
|
- ? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
|
|
|
- : parentAnchor
|
|
|
- if (newIndexToOldIndexMap[i] === 0) {
|
|
|
- // mount new
|
|
|
- mount(source, nextIndex, anchor)
|
|
|
- } else if (moved) {
|
|
|
- // move if:
|
|
|
- // There is no stable subsequence (e.g. a reverse)
|
|
|
- // OR current node is not among the stable sequence
|
|
|
- if (j < 0 || i !== increasingNewIndexSequence[j]) {
|
|
|
- insert(newBlocks[nextIndex].nodes, parent!, anchor)
|
|
|
- } else {
|
|
|
- j--
|
|
|
- }
|
|
|
+ for (let i = queuedBlocks.length - 1; i >= 0; i--) {
|
|
|
+ const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
|
|
|
+ relocateOrMountBlock(
|
|
|
+ blockIndex,
|
|
|
+ blockItem,
|
|
|
+ blockKey,
|
|
|
+ blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
|
|
|
+ const blockItem = getItem(source, i)
|
|
|
+ const blockKey = getKey(...blockItem)
|
|
|
+ relocateOrMountBlock(i, blockItem, blockKey, -1)
|
|
|
+ }
|
|
|
+
|
|
|
+ const useFastRemove = blocksToMount.length === newLength
|
|
|
+
|
|
|
+ for (const leftoverIndex of previousKeyIndexMap.values()) {
|
|
|
+ unmount(
|
|
|
+ oldBlocks[leftoverIndex],
|
|
|
+ !(useFastRemove && canUseFastRemove),
|
|
|
+ !useFastRemove,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (useFastRemove) {
|
|
|
+ for (const selector of selectors) {
|
|
|
+ selector.cleanup()
|
|
|
+ }
|
|
|
+ if (canUseFastRemove) {
|
|
|
+ parent!.textContent = ''
|
|
|
+ parent!.appendChild(parentAnchor)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ for (const [
|
|
|
+ blockIndex,
|
|
|
+ blockItem,
|
|
|
+ blockKey,
|
|
|
+ anchorOffset,
|
|
|
+ ] of blocksToMount) {
|
|
|
+ mount(
|
|
|
+ source,
|
|
|
+ blockIndex,
|
|
|
+ anchorOffset === -1
|
|
|
+ ? anchorFallback
|
|
|
+ : normalizeAnchor(newBlocks[anchorOffset].nodes),
|
|
|
+ blockItem,
|
|
|
+ blockKey,
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -312,13 +336,15 @@ export const createFor = (
|
|
|
source: ResolvedSource,
|
|
|
idx: number,
|
|
|
anchor: Node | undefined = parentAnchor,
|
|
|
+ [item, key, index] = getItem(source, idx),
|
|
|
+ key2 = getKey && getKey(item, key, index),
|
|
|
): ForBlock => {
|
|
|
- const [item, key, index] = getItem(source, idx)
|
|
|
const itemRef = shallowRef(item)
|
|
|
// avoid creating refs if the render fn doesn't need it
|
|
|
const keyRef = needKey ? shallowRef(key) : undefined
|
|
|
const indexRef = needIndex ? shallowRef(index) : undefined
|
|
|
|
|
|
+ currentKey = key2
|
|
|
let nodes: Block
|
|
|
let scope: EffectScope | undefined
|
|
|
if (isComponent) {
|
|
|
@@ -337,7 +363,7 @@ export const createFor = (
|
|
|
itemRef,
|
|
|
keyRef,
|
|
|
indexRef,
|
|
|
- getKey && getKey(item, key, index),
|
|
|
+ key2,
|
|
|
))
|
|
|
|
|
|
if (parent) insert(block.nodes, parent, anchor)
|
|
|
@@ -345,15 +371,6 @@ export const createFor = (
|
|
|
return block
|
|
|
}
|
|
|
|
|
|
- const tryPatchIndex = (source: any, idx: number) => {
|
|
|
- const block = oldBlocks[idx]
|
|
|
- const [item, key, index] = getItem(source, idx)
|
|
|
- if (block.key === getKey!(item, key, index)) {
|
|
|
- update((newBlocks[idx] = block), item)
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
const update = (
|
|
|
{ itemRef, keyRef, indexRef }: ForBlock,
|
|
|
newItem: any,
|