From: Johnson Chu Date: Thu, 10 Jul 2025 01:48:39 +0000 (+0800) Subject: perf(vapor): more efficient renderList update algorithm (#13279) X-Git-Tag: v3.6.0-alpha.1~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7cf9d9857d600dc43569f48c53d8e104e11b7ccf;p=thirdparty%2Fvuejs%2Fcore.git perf(vapor): more efficient renderList update algorithm (#13279) --- diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 6ea0dfeb45..426a5c56b5 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -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, + 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 - // 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, + blockKey: any, + anchorOffset: number, + ][] = [] + + const relocateOrMountBlock = ( + blockIndex: number, + blockItem: ReturnType, + 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,