Skip to content

Commit 682f6ca

Browse files
committed
perf(vapor): more efficient renderList items update algorithm
1 parent 5f8441d commit 682f6ca

File tree

1 file changed

+154
-137
lines changed

1 file changed

+154
-137
lines changed

packages/runtime-vapor/src/apiCreateFor.ts

+154-137
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
toReactive,
1111
watch,
1212
} from '@vue/reactivity'
13-
import { getSequence, isArray, isObject, isString } from '@vue/shared'
13+
import { isArray, isObject, isString } from '@vue/shared'
1414
import { createComment, createTextNode } from './dom/node'
1515
import {
1616
type Block,
@@ -142,149 +142,173 @@ export const createFor = (
142142
unmount(oldBlocks[i])
143143
}
144144
} else {
145-
let i = 0
146-
let e1 = oldLength - 1 // prev ending index
147-
let e2 = newLength - 1 // next ending index
148-
149-
// 1. sync from start
150-
// (a b) c
151-
// (a b) d e
152-
while (i <= e1 && i <= e2) {
153-
if (tryPatchIndex(source, i)) {
154-
i++
155-
} else {
156-
break
145+
const sharedBlockCount = Math.min(oldLength, newLength)
146+
const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
147+
const queuedBlocks: [
148+
blockIndex: number,
149+
blockItem: ReturnType<typeof getItem>,
150+
blockKey: any,
151+
][] = new Array(newLength)
152+
153+
let anchorFallback: Node = parentAnchor
154+
let endOffset = 0
155+
let startOffset = 0
156+
let queuedBlocksInsertIndex = 0
157+
let previousKeyIndexInsertIndex = 0
158+
159+
while (endOffset < sharedBlockCount) {
160+
const currentIndex = newLength - endOffset - 1
161+
const currentItem = getItem(source, currentIndex)
162+
const currentKey = getKey(...currentItem)
163+
const existingBlock = oldBlocks[oldLength - endOffset - 1]
164+
if (existingBlock.key === currentKey) {
165+
update(existingBlock, ...currentItem)
166+
newBlocks[currentIndex] = existingBlock
167+
endOffset++
168+
continue
169+
}
170+
if (endOffset !== 0) {
171+
anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
157172
}
173+
break
158174
}
159175

160-
// 2. sync from end
161-
// a (b c)
162-
// d e (b c)
163-
while (i <= e1 && i <= e2) {
164-
if (tryPatchIndex(source, i)) {
165-
e1--
166-
e2--
176+
while (startOffset < sharedBlockCount - endOffset) {
177+
const currentItem = getItem(source, startOffset)
178+
const currentKey = getKey(...currentItem)
179+
const previousBlock = oldBlocks[startOffset]
180+
const previousKey = previousBlock.key
181+
if (previousKey === currentKey) {
182+
update((newBlocks[startOffset] = previousBlock), currentItem[0])
167183
} else {
168-
break
184+
queuedBlocks[queuedBlocksInsertIndex++] = [
185+
startOffset,
186+
currentItem,
187+
currentKey,
188+
]
189+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
190+
previousKey,
191+
startOffset,
192+
]
169193
}
194+
startOffset++
170195
}
171196

172-
// 3. common sequence + mount
173-
// (a b)
174-
// (a b) c
175-
// i = 2, e1 = 1, e2 = 2
176-
// (a b)
177-
// c (a b)
178-
// i = 0, e1 = -1, e2 = 0
179-
if (i > e1) {
180-
if (i <= e2) {
181-
const nextPos = e2 + 1
182-
const anchor =
183-
nextPos < newLength
184-
? normalizeAnchor(newBlocks[nextPos].nodes)
185-
: parentAnchor
186-
while (i <= e2) {
187-
mount(source, i, anchor)
188-
i++
189-
}
190-
}
197+
for (let i = startOffset; i < oldLength - endOffset; i++) {
198+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
199+
oldBlocks[i].key,
200+
i,
201+
]
191202
}
192203

193-
// 4. common sequence + unmount
194-
// (a b) c
195-
// (a b)
196-
// i = 2, e1 = 2, e2 = 1
197-
// a (b c)
198-
// (b c)
199-
// i = 0, e1 = 0, e2 = -1
200-
else if (i > e2) {
201-
while (i <= e1) {
202-
unmount(oldBlocks[i])
203-
i++
204-
}
204+
const preparationBlockCount = Math.min(
205+
newLength - endOffset,
206+
sharedBlockCount,
207+
)
208+
for (let i = startOffset; i < preparationBlockCount; i++) {
209+
const blockItem = getItem(source, i)
210+
const blockKey = getKey(...blockItem)
211+
queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
205212
}
206213

207-
// 5. unknown sequence
208-
// [i ... e1 + 1]: a b [c d e] f g
209-
// [i ... e2 + 1]: a b [e d c h] f g
210-
// i = 2, e1 = 4, e2 = 5
211-
else {
212-
const s1 = i // prev starting index
213-
const s2 = i // next starting index
214-
215-
// 5.1 build key:index map for newChildren
216-
const keyToNewIndexMap = new Map()
217-
for (i = s2; i <= e2; i++) {
218-
keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
214+
if (!queuedBlocksInsertIndex && !previousKeyIndexInsertIndex) {
215+
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
216+
const blockItem = getItem(source, i)
217+
const blockKey = getKey(...blockItem)
218+
mount(source, i, anchorFallback, blockItem, blockKey)
219219
}
220-
221-
// 5.2 loop through old children left to be patched and try to patch
222-
// matching nodes & remove nodes that are no longer present
223-
let j
224-
let patched = 0
225-
const toBePatched = e2 - s2 + 1
226-
let moved = false
227-
// used to track whether any node has moved
228-
let maxNewIndexSoFar = 0
229-
// works as Map<newIndex, oldIndex>
230-
// Note that oldIndex is offset by +1
231-
// and oldIndex = 0 is a special value indicating the new node has
232-
// no corresponding old node.
233-
// used for determining longest stable subsequence
234-
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
235-
236-
for (i = s1; i <= e1; i++) {
237-
const prevBlock = oldBlocks[i]
238-
if (patched >= toBePatched) {
239-
// all new children have been patched so this can only be a removal
240-
unmount(prevBlock)
220+
} else {
221+
queuedBlocks.length = queuedBlocksInsertIndex
222+
previousKeyIndexPairs.length = previousKeyIndexInsertIndex
223+
224+
const previousKeyIndexMap = new Map(previousKeyIndexPairs)
225+
const blocksToMount: [
226+
blockIndex: number,
227+
blockItem: ReturnType<typeof getItem>,
228+
blockKey: any,
229+
anchorOffset: number,
230+
][] = []
231+
232+
const relocateOrMountBlock = (
233+
blockIndex: number,
234+
blockItem: ReturnType<typeof getItem>,
235+
blockKey: any,
236+
anchorOffset: number,
237+
) => {
238+
const previousIndex = previousKeyIndexMap.get(blockKey)
239+
if (previousIndex !== undefined) {
240+
const reusedBlock = (newBlocks[blockIndex] =
241+
oldBlocks[previousIndex])
242+
update(reusedBlock, ...blockItem)
243+
insert(
244+
reusedBlock,
245+
parent!,
246+
anchorOffset === -1
247+
? anchorFallback
248+
: normalizeAnchor(newBlocks[anchorOffset].nodes),
249+
)
250+
previousKeyIndexMap.delete(blockKey)
241251
} else {
242-
const newIndex = keyToNewIndexMap.get(prevBlock.key)
243-
if (newIndex == null) {
244-
unmount(prevBlock)
245-
} else {
246-
newIndexToOldIndexMap[newIndex - s2] = i + 1
247-
if (newIndex >= maxNewIndexSoFar) {
248-
maxNewIndexSoFar = newIndex
249-
} else {
250-
moved = true
251-
}
252-
update(
253-
(newBlocks[newIndex] = prevBlock),
254-
...getItem(source, newIndex),
255-
)
256-
patched++
257-
}
252+
blocksToMount.push([
253+
blockIndex,
254+
blockItem,
255+
blockKey,
256+
anchorOffset,
257+
])
258258
}
259259
}
260260

261-
// 5.3 move and mount
262-
// generate longest stable subsequence only when nodes have moved
263-
const increasingNewIndexSequence = moved
264-
? getSequence(newIndexToOldIndexMap)
265-
: []
266-
j = increasingNewIndexSequence.length - 1
267-
// looping backwards so that we can use last patched node as anchor
268-
for (i = toBePatched - 1; i >= 0; i--) {
269-
const nextIndex = s2 + i
270-
const anchor =
271-
nextIndex + 1 < newLength
272-
? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
273-
: parentAnchor
274-
if (newIndexToOldIndexMap[i] === 0) {
275-
// mount new
276-
mount(source, nextIndex, anchor)
277-
} else if (moved) {
278-
// move if:
279-
// There is no stable subsequence (e.g. a reverse)
280-
// OR current node is not among the stable sequence
281-
if (j < 0 || i !== increasingNewIndexSequence[j]) {
282-
insert(newBlocks[nextIndex].nodes, parent!, anchor)
283-
} else {
284-
j--
285-
}
261+
for (let i = queuedBlocks.length - 1; i >= 0; i--) {
262+
const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
263+
relocateOrMountBlock(
264+
blockIndex,
265+
blockItem,
266+
blockKey,
267+
blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
268+
)
269+
}
270+
271+
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
272+
const blockItem = getItem(source, i)
273+
const blockKey = getKey(...blockItem)
274+
relocateOrMountBlock(i, blockItem, blockKey, -1)
275+
}
276+
277+
const useFastRemove = blocksToMount.length === newLength
278+
279+
for (const leftoverIndex of previousKeyIndexMap.values()) {
280+
unmount(
281+
oldBlocks[leftoverIndex],
282+
!(useFastRemove && canUseFastRemove),
283+
!useFastRemove,
284+
)
285+
}
286+
if (useFastRemove) {
287+
for (const selector of selectors) {
288+
selector.cleanup()
289+
}
290+
if (canUseFastRemove) {
291+
parent!.textContent = ''
292+
parent!.appendChild(parentAnchor)
286293
}
287294
}
295+
296+
for (const [
297+
blockIndex,
298+
blockItem,
299+
blockKey,
300+
anchorOffset,
301+
] of blocksToMount) {
302+
mount(
303+
source,
304+
blockIndex,
305+
anchorOffset === -1
306+
? anchorFallback
307+
: normalizeAnchor(newBlocks[anchorOffset].nodes),
308+
blockItem,
309+
blockKey,
310+
)
311+
}
288312
}
289313
}
290314
}
@@ -304,13 +328,15 @@ export const createFor = (
304328
source: ResolvedSource,
305329
idx: number,
306330
anchor: Node | undefined = parentAnchor,
331+
[item, key, index] = getItem(source, idx),
332+
key2 = getKey && getKey(item, key, index),
307333
): ForBlock => {
308-
const [item, key, index] = getItem(source, idx)
309334
const itemRef = shallowRef(item)
310335
// avoid creating refs if the render fn doesn't need it
311336
const keyRef = needKey ? shallowRef(key) : undefined
312337
const indexRef = needIndex ? shallowRef(index) : undefined
313338

339+
currentKey = key2
314340
let nodes: Block
315341
let scope: EffectScope | undefined
316342
if (isComponent) {
@@ -329,23 +355,14 @@ export const createFor = (
329355
itemRef,
330356
keyRef,
331357
indexRef,
332-
getKey && getKey(item, key, index),
358+
key2,
333359
))
334360

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

337363
return block
338364
}
339365

340-
const tryPatchIndex = (source: any, idx: number) => {
341-
const block = oldBlocks[idx]
342-
const [item, key, index] = getItem(source, idx)
343-
if (block.key === getKey!(item, key, index)) {
344-
update((newBlocks[idx] = block), item)
345-
return true
346-
}
347-
}
348-
349366
const update = (
350367
{ itemRef, keyRef, indexRef }: ForBlock,
351368
newItem: any,

0 commit comments

Comments
 (0)