Skip to content

Commit c4f41ee

Browse files
fix(vapor): avoid unnecessary block movement in renderList (#13722)
1 parent 27632bb commit c4f41ee

File tree

1 file changed

+153
-134
lines changed

1 file changed

+153
-134
lines changed

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 153 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import {
3434
class ForBlock extends VaporFragment {
3535
scope: EffectScope | undefined
3636
key: any
37+
prev?: ForBlock
38+
next?: ForBlock
39+
prevAnchor?: ForBlock
3740

3841
itemRef: ShallowRef<any>
3942
keyRef: ShallowRef<any> | undefined
@@ -90,7 +93,7 @@ export const createFor = (
9093
let oldBlocks: ForBlock[] = []
9194
let newBlocks: ForBlock[]
9295
let parent: ParentNode | undefined | null
93-
// useSelector only
96+
// createSelector only
9497
let currentKey: any
9598
// TODO handle this in hydration
9699
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
@@ -171,169 +174,169 @@ export const createFor = (
171174
}
172175
}
173176

174-
const sharedBlockCount = Math.min(oldLength, newLength)
175-
const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
177+
const commonLength = Math.min(oldLength, newLength)
178+
const oldKeyIndexPairs: [any, number][] = new Array(oldLength)
176179
const queuedBlocks: [
177-
blockIndex: number,
178-
blockItem: ReturnType<typeof getItem>,
179-
blockKey: any,
180+
index: number,
181+
item: ReturnType<typeof getItem>,
182+
key: any,
180183
][] = new Array(newLength)
181184

182-
let anchorFallback: Node = parentAnchor
183185
let endOffset = 0
184-
let startOffset = 0
185-
let queuedBlocksInsertIndex = 0
186-
let previousKeyIndexInsertIndex = 0
186+
let queuedBlocksLength = 0
187+
let oldKeyIndexPairsLength = 0
187188

188-
while (endOffset < sharedBlockCount) {
189-
const currentIndex = newLength - endOffset - 1
190-
const currentItem = getItem(source, currentIndex)
191-
const currentKey = getKey(...currentItem)
189+
while (endOffset < commonLength) {
190+
const index = newLength - endOffset - 1
191+
const item = getItem(source, index)
192+
const key = getKey(...item)
192193
const existingBlock = oldBlocks[oldLength - endOffset - 1]
193-
if (existingBlock.key === currentKey) {
194-
update(existingBlock, ...currentItem)
195-
newBlocks[currentIndex] = existingBlock
196-
endOffset++
197-
continue
198-
}
199-
break
194+
if (existingBlock.key !== key) break
195+
update(existingBlock, ...item)
196+
newBlocks[index] = existingBlock
197+
endOffset++
200198
}
201199

202-
if (endOffset !== 0) {
203-
anchorFallback = normalizeAnchor(
204-
newBlocks[newLength - endOffset].nodes,
205-
)
206-
}
200+
const e1 = commonLength - endOffset
201+
const e2 = oldLength - endOffset
202+
const e3 = newLength - endOffset
207203

208-
while (startOffset < sharedBlockCount - endOffset) {
209-
const currentItem = getItem(source, startOffset)
204+
for (let i = 0; i < e1; i++) {
205+
const currentItem = getItem(source, i)
210206
const currentKey = getKey(...currentItem)
211-
const previousBlock = oldBlocks[startOffset]
212-
const previousKey = previousBlock.key
213-
if (previousKey === currentKey) {
214-
update((newBlocks[startOffset] = previousBlock), currentItem[0])
207+
const oldBlock = oldBlocks[i]
208+
const oldKey = oldBlock.key
209+
if (oldKey === currentKey) {
210+
update((newBlocks[i] = oldBlock), currentItem[0])
215211
} else {
216-
queuedBlocks[queuedBlocksInsertIndex++] = [
217-
startOffset,
218-
currentItem,
219-
currentKey,
220-
]
221-
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
222-
previousKey,
223-
startOffset,
224-
]
212+
queuedBlocks[queuedBlocksLength++] = [i, currentItem, currentKey]
213+
oldKeyIndexPairs[oldKeyIndexPairsLength++] = [oldKey, i]
225214
}
226-
startOffset++
227215
}
228216

229-
for (let i = startOffset; i < oldLength - endOffset; i++) {
230-
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
231-
oldBlocks[i].key,
232-
i,
233-
]
217+
for (let i = e1; i < e2; i++) {
218+
oldKeyIndexPairs[oldKeyIndexPairsLength++] = [oldBlocks[i].key, i]
234219
}
235220

236-
const preparationBlockCount = Math.min(
237-
newLength - endOffset,
238-
sharedBlockCount,
239-
)
240-
for (let i = startOffset; i < preparationBlockCount; i++) {
221+
for (let i = e1; i < e3; i++) {
241222
const blockItem = getItem(source, i)
242223
const blockKey = getKey(...blockItem)
243-
queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
224+
queuedBlocks[queuedBlocksLength++] = [i, blockItem, blockKey]
244225
}
245226

246-
if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) {
247-
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
248-
const blockItem = getItem(source, i)
249-
const blockKey = getKey(...blockItem)
250-
mount(source, i, anchorFallback, blockItem, blockKey)
251-
}
252-
} else {
253-
queuedBlocks.length = queuedBlocksInsertIndex
254-
previousKeyIndexPairs.length = previousKeyIndexInsertIndex
255-
256-
const previousKeyIndexMap = new Map(previousKeyIndexPairs)
257-
const operations: (() => void)[] = []
258-
259-
let mountCounter = 0
260-
const relocateOrMountBlock = (
261-
blockIndex: number,
262-
blockItem: ReturnType<typeof getItem>,
263-
blockKey: any,
264-
anchorOffset: number,
265-
) => {
266-
const previousIndex = previousKeyIndexMap.get(blockKey)
267-
if (previousIndex !== undefined) {
268-
const reusedBlock = (newBlocks[blockIndex] =
269-
oldBlocks[previousIndex])
270-
update(reusedBlock, ...blockItem)
271-
previousKeyIndexMap.delete(blockKey)
272-
if (previousIndex !== blockIndex) {
273-
operations.push(() =>
274-
insert(
275-
reusedBlock,
276-
parent!,
277-
anchorOffset === -1
278-
? anchorFallback
279-
: normalizeAnchor(newBlocks[anchorOffset].nodes),
280-
),
281-
)
282-
}
283-
} else {
284-
mountCounter++
285-
operations.push(() =>
286-
mount(
287-
source,
288-
blockIndex,
289-
anchorOffset === -1
290-
? anchorFallback
291-
: normalizeAnchor(newBlocks[anchorOffset].nodes),
292-
blockItem,
293-
blockKey,
294-
),
295-
)
296-
}
297-
}
227+
queuedBlocks.length = queuedBlocksLength
228+
oldKeyIndexPairs.length = oldKeyIndexPairsLength
298229

299-
for (let i = queuedBlocks.length - 1; i >= 0; i--) {
300-
const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
301-
relocateOrMountBlock(
302-
blockIndex,
303-
blockItem,
304-
blockKey,
305-
blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
306-
)
307-
}
230+
interface MountOper {
231+
source: ResolvedSource
232+
index: number
233+
item: ReturnType<typeof getItem>
234+
key: any
235+
}
236+
interface MoveOper {
237+
index: number
238+
block: ForBlock
239+
}
240+
241+
const oldKeyIndexMap = new Map(oldKeyIndexPairs)
242+
const opers: (MountOper | MoveOper)[] = new Array(queuedBlocks.length)
308243

309-
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
310-
const blockItem = getItem(source, i)
311-
const blockKey = getKey(...blockItem)
312-
relocateOrMountBlock(i, blockItem, blockKey, -1)
244+
let mountCounter = 0
245+
let opersLength = 0
246+
247+
for (let i = queuedBlocks.length - 1; i >= 0; i--) {
248+
const [index, item, key] = queuedBlocks[i]
249+
const oldIndex = oldKeyIndexMap.get(key)
250+
if (oldIndex !== undefined) {
251+
oldKeyIndexMap.delete(key)
252+
const reusedBlock = (newBlocks[index] = oldBlocks[oldIndex])
253+
update(reusedBlock, ...item)
254+
opers[opersLength++] = { index, block: reusedBlock }
255+
} else {
256+
mountCounter++
257+
opers[opersLength++] = { source, index, item, key }
313258
}
259+
}
260+
261+
const useFastRemove = mountCounter === newLength
314262

315-
const useFastRemove = mountCounter === newLength
263+
for (const leftoverIndex of oldKeyIndexMap.values()) {
264+
unmount(
265+
oldBlocks[leftoverIndex],
266+
!(useFastRemove && canUseFastRemove),
267+
!useFastRemove,
268+
)
269+
}
270+
if (useFastRemove) {
271+
for (const selector of selectors) {
272+
selector.cleanup()
273+
}
274+
if (canUseFastRemove) {
275+
parent!.textContent = ''
276+
parent!.appendChild(parentAnchor)
277+
}
278+
}
316279

317-
for (const leftoverIndex of previousKeyIndexMap.values()) {
318-
unmount(
319-
oldBlocks[leftoverIndex],
320-
!(useFastRemove && canUseFastRemove),
321-
!useFastRemove,
280+
if (opers.length === mountCounter) {
281+
for (const { source, index, item, key } of opers as MountOper[]) {
282+
mount(
283+
source,
284+
index,
285+
index < newLength - 1
286+
? normalizeAnchor(newBlocks[index + 1].nodes)
287+
: parentAnchor,
288+
item,
289+
key,
322290
)
323291
}
324-
if (useFastRemove) {
325-
for (const selector of selectors) {
326-
selector.cleanup()
292+
} else if (opers.length) {
293+
let anchor = oldBlocks[0]
294+
let blocksTail: ForBlock | undefined
295+
for (let i = 0; i < oldLength; i++) {
296+
const block = oldBlocks[i]
297+
if (oldKeyIndexMap.has(block.key)) {
298+
continue
327299
}
328-
if (canUseFastRemove) {
329-
parent!.textContent = ''
330-
parent!.appendChild(parentAnchor)
300+
block.prevAnchor = anchor
301+
anchor = oldBlocks[i + 1]
302+
if (blocksTail !== undefined) {
303+
blocksTail.next = block
304+
block.prev = blocksTail
331305
}
306+
blocksTail = block
332307
}
333-
334-
// perform mount and move operations
335-
for (const action of operations) {
336-
action()
308+
for (const action of opers) {
309+
const { index } = action
310+
if (index < newLength - 1) {
311+
const nextBlock = newBlocks[index + 1]
312+
let anchorNode = normalizeAnchor(nextBlock.prevAnchor!.nodes)
313+
if (!anchorNode.parentNode)
314+
anchorNode = normalizeAnchor(nextBlock.nodes)
315+
if ('source' in action) {
316+
const { item, key } = action
317+
const block = mount(source, index, anchorNode, item, key)
318+
moveLink(block, nextBlock.prev, nextBlock)
319+
} else if (action.block.next !== nextBlock) {
320+
insert(action.block, parent!, anchorNode)
321+
moveLink(action.block, nextBlock.prev, nextBlock)
322+
}
323+
} else if ('source' in action) {
324+
const { item, key } = action
325+
const block = mount(source, index, parentAnchor, item, key)
326+
moveLink(block, blocksTail)
327+
blocksTail = block
328+
} else if (action.block.next !== undefined) {
329+
let anchorNode = anchor
330+
? normalizeAnchor(anchor.nodes)
331+
: parentAnchor
332+
if (!anchorNode.parentNode) anchorNode = parentAnchor
333+
insert(action.block, parent!, anchorNode)
334+
moveLink(action.block, blocksTail)
335+
blocksTail = action.block
336+
}
337+
}
338+
for (const block of newBlocks) {
339+
block.prevAnchor = block.next = block.prev = undefined
337340
}
338341
}
339342
}
@@ -486,6 +489,22 @@ export const createFor = (
486489
}
487490
}
488491

492+
function moveLink(block: ForBlock, newPrev?: ForBlock, newNext?: ForBlock) {
493+
const { prev: oldPrev, next: oldNext } = block
494+
if (oldPrev) oldPrev.next = oldNext
495+
if (oldNext) {
496+
oldNext.prev = oldPrev
497+
if (block.prevAnchor !== block) {
498+
oldNext.prevAnchor = block.prevAnchor
499+
}
500+
}
501+
if (newPrev) newPrev.next = block
502+
if (newNext) newNext.prev = block
503+
block.prev = newPrev
504+
block.next = newNext
505+
block.prevAnchor = block
506+
}
507+
489508
export function createForSlots(
490509
rawSource: Source,
491510
getSlot: (item: any, key: any, index?: number) => DynamicSlot,

0 commit comments

Comments
 (0)