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