Skip to content

perf(vapor): more efficient renderList update algorithm #13279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: vapor
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 154 additions & 137 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
shallowRef,
toReactive,
} 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,
Expand Down Expand Up @@ -132,149 +132,173 @@
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,
)
}
}
}
}
Expand All @@ -294,13 +318,15 @@
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

Check failure on line 329 in packages/runtime-vapor/src/apiCreateFor.ts

View workflow job for this annotation

GitHub Actions / test / unit-test

packages/runtime-vapor/__tests__/for.spec.ts > createFor > de-structured value

ReferenceError: currentKey is not defined ❯ mount packages/runtime-vapor/src/apiCreateFor.ts:329:5 ❯ renderList packages/runtime-vapor/src/apiCreateFor.ts:103:9 ❯ ReactiveEffect.renderEffectFn [as fn] packages/runtime-vapor/src/renderEffect.ts:41:11 ❯ ReactiveEffect.run packages/reactivity/src/effect.ts:125:19 ❯ renderEffect packages/runtime-vapor/src/renderEffect.ts:67:10 ❯ createFor packages/runtime-vapor/src/apiCreateFor.ts:381:5 ❯ packages/runtime-vapor/__tests__/for.spec.ts:195:18 ❯ callWithErrorHandling packages/runtime-core/src/errorHandling.ts:76:19 ❯ createComponent packages/runtime-vapor/src/component.ts:204:7 ❯ mountApp packages/runtime-vapor/src/apiCreateApp.ts:39:20

Check failure on line 329 in packages/runtime-vapor/src/apiCreateFor.ts

View workflow job for this annotation

GitHub Actions / test / unit-test

packages/runtime-vapor/__tests__/for.spec.ts > createFor > object source

ReferenceError: currentKey is not defined ❯ mount packages/runtime-vapor/src/apiCreateFor.ts:329:5 ❯ renderList packages/runtime-vapor/src/apiCreateFor.ts:103:9 ❯ ReactiveEffect.renderEffectFn [as fn] packages/runtime-vapor/src/renderEffect.ts:41:11 ❯ ReactiveEffect.run packages/reactivity/src/effect.ts:125:19 ❯ renderEffect packages/runtime-vapor/src/renderEffect.ts:67:10 ❯ createFor packages/runtime-vapor/src/apiCreateFor.ts:381:5 ❯ packages/runtime-vapor/__tests__/for.spec.ts:129:18 ❯ callWithErrorHandling packages/runtime-core/src/errorHandling.ts:76:19 ❯ createComponent packages/runtime-vapor/src/component.ts:204:7 ❯ mountApp packages/runtime-vapor/src/apiCreateApp.ts:39:20

Check failure on line 329 in packages/runtime-vapor/src/apiCreateFor.ts

View workflow job for this annotation

GitHub Actions / test / unit-test

packages/runtime-vapor/__tests__/for.spec.ts > createFor > number source

ReferenceError: currentKey is not defined ❯ mount packages/runtime-vapor/src/apiCreateFor.ts:329:5 ❯ renderList packages/runtime-vapor/src/apiCreateFor.ts:103:9 ❯ ReactiveEffect.renderEffectFn [as fn] packages/runtime-vapor/src/renderEffect.ts:41:11 ❯ ReactiveEffect.run packages/reactivity/src/effect.ts:125:19 ❯ renderEffect packages/runtime-vapor/src/renderEffect.ts:67:10 ❯ createFor packages/runtime-vapor/src/apiCreateFor.ts:381:5 ❯ packages/runtime-vapor/__tests__/for.spec.ts:85:18 ❯ callWithErrorHandling packages/runtime-core/src/errorHandling.ts:76:19 ❯ createComponent packages/runtime-vapor/src/component.ts:204:7 ❯ mountApp packages/runtime-vapor/src/apiCreateApp.ts:39:20

Check failure on line 329 in packages/runtime-vapor/src/apiCreateFor.ts

View workflow job for this annotation

GitHub Actions / test / unit-test

packages/runtime-vapor/__tests__/for.spec.ts > createFor > array source

ReferenceError: currentKey is not defined ❯ mount packages/runtime-vapor/src/apiCreateFor.ts:329:5 ❯ renderList packages/runtime-vapor/src/apiCreateFor.ts:103:9 ❯ ReactiveEffect.renderEffectFn [as fn] packages/runtime-vapor/src/renderEffect.ts:41:11 ❯ ReactiveEffect.run packages/reactivity/src/effect.ts:125:19 ❯ renderEffect packages/runtime-vapor/src/renderEffect.ts:67:10 ❯ createFor packages/runtime-vapor/src/apiCreateFor.ts:381:5 ❯ packages/runtime-vapor/__tests__/for.spec.ts:20:18 ❯ callWithErrorHandling packages/runtime-core/src/errorHandling.ts:76:19 ❯ createComponent packages/runtime-vapor/src/component.ts:204:7 ❯ mountApp packages/runtime-vapor/src/apiCreateApp.ts:39:20

Check failure on line 329 in packages/runtime-vapor/src/apiCreateFor.ts

View workflow job for this annotation

GitHub Actions / test / unit-test

packages/runtime-vapor/__tests__/apiLifecycle.spec.ts > api: lifecycle hooks > unmount hooks when nested in for blocks

ReferenceError: currentKey is not defined ❯ mount packages/runtime-vapor/src/apiCreateFor.ts:329:5 ❯ renderList packages/runtime-vapor/src/apiCreateFor.ts:103:9 ❯ ReactiveEffect.renderEffectFn [as fn] packages/runtime-vapor/src/renderEffect.ts:41:11 ❯ ReactiveEffect.run packages/reactivity/src/effect.ts:125:19 ❯ renderEffect packages/runtime-vapor/src/renderEffect.ts:67:10 ❯ createFor packages/runtime-vapor/src/apiCreateFor.ts:381:5 ❯ setup packages/runtime-vapor/__tests__/apiLifecycle.spec.ts:585:20 ❯ callWithErrorHandling packages/runtime-core/src/errorHandling.ts:76:19 ❯ createComponent packages/runtime-vapor/src/component.ts:204:7 ❯ mountApp packages/runtime-vapor/src/apiCreateApp.ts:39:20
let nodes: Block
let scope: EffectScope | undefined
if (isComponent) {
Expand All @@ -319,23 +345,14 @@
itemRef,
keyRef,
indexRef,
getKey && getKey(item, key, index),
key2,
))

if (parent) insert(block.nodes, parent, anchor)

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,
Expand Down
Loading
Loading