]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(vapor): more efficient renderList update algorithm (#13279)
authorJohnson Chu <johnsoncodehk@gmail.com>
Thu, 10 Jul 2025 01:48:39 +0000 (09:48 +0800)
committerGitHub <noreply@github.com>
Thu, 10 Jul 2025 01:48:39 +0000 (18:48 -0700)
packages/runtime-vapor/src/apiCreateFor.ts

index 6ea0dfeb451b6ef4f642a7ed3444f0c572a7b71f..426a5c56b5b1ba226f352a31985c524ee1b87cf7 100644 (file)
@@ -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,