diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index bdd2af7b348..f91dcb38525 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -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 keyRef: ShallowRef | undefined @@ -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() @@ -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, - blockKey: any, + index: number, + item: ReturnType, + 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, - 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 + 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 } } } @@ -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,