Skip to content
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0c2f404
fix(vapor): avoid unnecessary block movement in renderList
johnsoncodehk Jul 31, 2025
6a86914
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
825269f
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
c9f0c65
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
f57c663
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
6626118
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
3aa4f07
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
77465d8
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
baccbf4
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
013ae16
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
65a5b49
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
c46ba39
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
64de0ff
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
2dad05d
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
9be799f
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
7ac570b
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
98a5124
Revert "Update apiCreateFor.ts"
johnsoncodehk Jul 31, 2025
bbe27e8
Update apiCreateFor.ts
johnsoncodehk Jul 31, 2025
6f93ca9
add prevAnchor
johnsoncodehk Aug 1, 2025
446a650
add fallbackAnchor
johnsoncodehk Aug 1, 2025
2e784ff
Update vitest.config.ts
johnsoncodehk Aug 1, 2025
3313006
handle non-transition
johnsoncodehk Aug 1, 2025
5cd0610
Update apiCreateFor.ts
johnsoncodehk Aug 1, 2025
19e7a44
inherit prevAnchor on move
johnsoncodehk Aug 1, 2025
905fe7e
Update apiCreateFor.ts
johnsoncodehk Aug 19, 2025
4216ac9
Make prev, next, prevAnchor optional
johnsoncodehk Aug 19, 2025
f6ff26c
Update apiCreateFor.ts
johnsoncodehk Aug 19, 2025
64a4c2e
Update apiCreateFor.ts
johnsoncodehk Aug 19, 2025
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
287 changes: 153 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
next?: ForBlock
prevAnchor?: ForBlock

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,169 @@ 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 {
source: ResolvedSource
index: number
item: ReturnType<typeof getItem>
key: any
}
interface MoveOper {
index: number
block: ForBlock
}

const oldKeyIndexMap = new Map(oldKeyIndexPairs)
const opers: (MountOper | MoveOper)[] = 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++] = { index, block: reusedBlock }
} else {
mountCounter++
opers[opersLength++] = { source, index, item, key }
}
}

const useFastRemove = mountCounter === newLength

const useFastRemove = mountCounter === newLength
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)
}
}

for (const leftoverIndex of previousKeyIndexMap.values()) {
unmount(
oldBlocks[leftoverIndex],
!(useFastRemove && canUseFastRemove),
!useFastRemove,
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) {
const { index } = action
if (index < newLength - 1) {
const nextBlock = newBlocks[index + 1]
let anchorNode = normalizeAnchor(nextBlock.prevAnchor!.nodes)
if (!anchorNode.parentNode)
anchorNode = normalizeAnchor(nextBlock.nodes)
if ('source' in action) {
const { item, key } = action
const block = mount(source, index, anchorNode, item, key)
moveLink(block, nextBlock.prev, nextBlock)
} else if (action.block.next !== nextBlock) {
insert(action.block, parent!, anchorNode)
moveLink(action.block, nextBlock.prev, nextBlock)
}
} else if ('source' in action) {
const { item, key } = action
const block = mount(source, index, parentAnchor, item, key)
moveLink(block, blocksTail)
blocksTail = block
} else if (action.block.next !== undefined) {
let anchorNode = anchor
? normalizeAnchor(anchor.nodes)
: parentAnchor
if (!anchorNode.parentNode) anchorNode = parentAnchor
insert(action.block, parent!, anchorNode)
moveLink(action.block, blocksTail)
blocksTail = action.block
}
}
for (const block of newBlocks) {
block.prevAnchor = block.next = block.prev = undefined
}
}
}
Expand Down Expand Up @@ -486,6 +489,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
Loading