Skip to content

fix(vapor): avoid unnecessary block movement in renderList #13722

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 24 commits into
base: minor
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
296 changes: 162 additions & 134 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
key: any
prev: ForBlock | undefined
next: ForBlock | undefined
prevAnchor: ForBlock | undefined

itemRef: ShallowRef<any>
keyRef: ShallowRef<any> | undefined
Expand Down Expand Up @@ -90,7 +93,7 @@ export const createFor = (
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
// useSelector only
// createSelector only
let currentKey: any
// TODO handle this in hydration
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
Expand Down Expand Up @@ -171,169 +174,178 @@ export const createFor = (
}
}

const sharedBlockCount = Math.min(oldLength, newLength)
const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
const commonLength = Math.min(oldLength, newLength)
const oldKeyIndexPairs: [any, number][] = new Array(oldLength)
const queuedBlocks: [
blockIndex: number,
blockItem: ReturnType<typeof getItem>,
blockKey: any,
index: number,
item: ReturnType<typeof getItem>,
key: any,
][] = new Array(newLength)

let anchorFallback: Node = parentAnchor
let endOffset = 0
let startOffset = 0
let queuedBlocksInsertIndex = 0
let previousKeyIndexInsertIndex = 0
let queuedBlocksLength = 0
let oldKeyIndexPairsLength = 0

while (endOffset < sharedBlockCount) {
const currentIndex = newLength - endOffset - 1
const currentItem = getItem(source, currentIndex)
const currentKey = getKey(...currentItem)
while (endOffset < commonLength) {
const index = newLength - endOffset - 1
const item = getItem(source, index)
const key = getKey(...item)
const existingBlock = oldBlocks[oldLength - endOffset - 1]
if (existingBlock.key === currentKey) {
update(existingBlock, ...currentItem)
newBlocks[currentIndex] = existingBlock
endOffset++
continue
}
break
if (existingBlock.key !== key) break
update(existingBlock, ...item)
newBlocks[index] = existingBlock
endOffset++
}

if (endOffset !== 0) {
anchorFallback = normalizeAnchor(
newBlocks[newLength - endOffset].nodes,
)
}
const e1 = commonLength - endOffset
const e2 = oldLength - endOffset
const e3 = newLength - endOffset

while (startOffset < sharedBlockCount - endOffset) {
const currentItem = getItem(source, startOffset)
for (let i = 0; i < e1; i++) {
const currentItem = getItem(source, i)
const currentKey = getKey(...currentItem)
const previousBlock = oldBlocks[startOffset]
const previousKey = previousBlock.key
if (previousKey === currentKey) {
update((newBlocks[startOffset] = previousBlock), currentItem[0])
const oldBlock = oldBlocks[i]
const oldKey = oldBlock.key
if (oldKey === currentKey) {
update((newBlocks[i] = oldBlock), currentItem[0])
} else {
queuedBlocks[queuedBlocksInsertIndex++] = [
startOffset,
currentItem,
currentKey,
]
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
previousKey,
startOffset,
]
queuedBlocks[queuedBlocksLength++] = [i, currentItem, currentKey]
oldKeyIndexPairs[oldKeyIndexPairsLength++] = [oldKey, i]
}
startOffset++
}

for (let i = startOffset; i < oldLength - endOffset; i++) {
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
oldBlocks[i].key,
i,
]
for (let i = e1; i < e2; i++) {
oldKeyIndexPairs[oldKeyIndexPairsLength++] = [oldBlocks[i].key, i]
}

const preparationBlockCount = Math.min(
newLength - endOffset,
sharedBlockCount,
)
for (let i = startOffset; i < preparationBlockCount; i++) {
for (let i = e1; i < e3; i++) {
const blockItem = getItem(source, i)
const blockKey = getKey(...blockItem)
queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
queuedBlocks[queuedBlocksLength++] = [i, blockItem, blockKey]
}

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)
}
} else {
queuedBlocks.length = queuedBlocksInsertIndex
previousKeyIndexPairs.length = previousKeyIndexInsertIndex

const previousKeyIndexMap = new Map(previousKeyIndexPairs)
const operations: (() => void)[] = []

let mountCounter = 0
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)
previousKeyIndexMap.delete(blockKey)
if (previousIndex !== blockIndex) {
operations.push(() =>
insert(
reusedBlock,
parent!,
anchorOffset === -1
? anchorFallback
: normalizeAnchor(newBlocks[anchorOffset].nodes),
),
)
}
} else {
mountCounter++
operations.push(() =>
mount(
source,
blockIndex,
anchorOffset === -1
? anchorFallback
: normalizeAnchor(newBlocks[anchorOffset].nodes),
blockItem,
blockKey,
),
)
}
}
queuedBlocks.length = queuedBlocksLength
oldKeyIndexPairs.length = oldKeyIndexPairsLength

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,
)
}
interface MountOper {
type: 'mount'
source: ResolvedSource
index: number
item: ReturnType<typeof getItem>
key: any
}
interface InsertOper {
type: 'insert'
index: number
block: ForBlock
}

const oldKeyIndexMap = new Map(oldKeyIndexPairs)
const opers: (MountOper | InsertOper)[] = new Array(queuedBlocks.length)

for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
const blockItem = getItem(source, i)
const blockKey = getKey(...blockItem)
relocateOrMountBlock(i, blockItem, blockKey, -1)
let mountCounter = 0
let opersLength = 0

for (let i = queuedBlocks.length - 1; i >= 0; i--) {
const [index, item, key] = queuedBlocks[i]
const oldIndex = oldKeyIndexMap.get(key)
if (oldIndex !== undefined) {
oldKeyIndexMap.delete(key)
const reusedBlock = (newBlocks[index] = oldBlocks[oldIndex])
update(reusedBlock, ...item)
opers[opersLength++] = { type: 'insert', index, block: reusedBlock }
} else {
mountCounter++
opers[opersLength++] = { type: 'mount', source, index, item, key }
}
}

const useFastRemove = mountCounter === newLength
const useFastRemove = mountCounter === newLength

for (const leftoverIndex of previousKeyIndexMap.values()) {
unmount(
oldBlocks[leftoverIndex],
!(useFastRemove && canUseFastRemove),
!useFastRemove,
for (const leftoverIndex of oldKeyIndexMap.values()) {
unmount(
oldBlocks[leftoverIndex],
!(useFastRemove && canUseFastRemove),
!useFastRemove,
)
}
if (useFastRemove) {
for (const selector of selectors) {
selector.cleanup()
}
if (canUseFastRemove) {
parent!.textContent = ''
parent!.appendChild(parentAnchor)
}
}

if (opers.length === mountCounter) {
for (const { source, index, item, key } of opers as MountOper[]) {
mount(
source,
index,
index < newLength - 1
? normalizeAnchor(newBlocks[index + 1].nodes)
: parentAnchor,
item,
key,
)
}
if (useFastRemove) {
for (const selector of selectors) {
selector.cleanup()
} else if (opers.length) {
let anchor = oldBlocks[0]
let blocksTail: ForBlock | undefined
for (let i = 0; i < oldLength; i++) {
const block = oldBlocks[i]
if (oldKeyIndexMap.has(block.key)) {
continue
}
if (canUseFastRemove) {
parent!.textContent = ''
parent!.appendChild(parentAnchor)
block.prevAnchor = anchor
anchor = oldBlocks[i + 1]
if (blocksTail !== undefined) {
blocksTail.next = block
block.prev = blocksTail
}
blocksTail = block
}

// perform mount and move operations
for (const action of operations) {
action()
for (const action of opers) {
if (action.type === 'mount') {
const { source, index, item, key } = action
if (index < newLength - 1) {
const nextBlock = newBlocks[index + 1]
let anchorNode = normalizeAnchor(nextBlock.prevAnchor!.nodes)
if (!anchorNode.parentNode)
anchorNode = normalizeAnchor(nextBlock.nodes)
const block = mount(source, index, anchorNode, item, key)
moveLink(block, nextBlock.prev, nextBlock)
} else {
const block = mount(source, index, parentAnchor, item, key)
moveLink(block, blocksTail)
blocksTail = block
}
} else {
const { index, block } = action
if (index < newLength - 1) {
const nextBlock = newBlocks[index + 1]
if (block.next !== nextBlock) {
let anchorNode = normalizeAnchor(nextBlock.prevAnchor!.nodes)
if (!anchorNode.parentNode)
anchorNode = normalizeAnchor(nextBlock.nodes)
insert(block, parent!, anchorNode)
moveLink(block, nextBlock.prev, nextBlock)
}
} else if (block.next !== undefined) {
let anchorNode: Node = anchor
? normalizeAnchor(anchor.nodes)
: parentAnchor
if (!anchorNode.parentNode) anchorNode = parentAnchor
insert(block, parent!, anchorNode)
moveLink(block, blocksTail)
blocksTail = block
}
}
}
for (const block of newBlocks) {
block.prevAnchor = block.next = block.prev = undefined
}
}
}
Expand Down Expand Up @@ -486,6 +498,22 @@ export const createFor = (
}
}

function moveLink(block: ForBlock, newPrev?: ForBlock, newNext?: ForBlock) {
const { prev: oldPrev, next: oldNext } = block
if (oldPrev) oldPrev.next = oldNext
if (oldNext) {
oldNext.prev = oldPrev
if (block.prevAnchor !== block) {
oldNext.prevAnchor = block.prevAnchor
}
}
if (newPrev) newPrev.next = block
if (newNext) newNext.prev = block
block.prev = newPrev
block.next = newNext
block.prevAnchor = block
}

export function createForSlots(
rawSource: Source,
getSlot: (item: any, key: any, index?: number) => DynamicSlot,
Expand Down