Skip to content

Commit e89e362

Browse files
committed
perf(vapor): more efficient renderList items update algorithm
1 parent 9ab8e4c commit e89e362

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
@@ -9,7 +9,7 @@ import {
99
shallowRef,
1010
toReactive,
1111
} from '@vue/reactivity'
12-
import { getSequence, isArray, isObject, isString } from '@vue/shared'
12+
import { isArray, isObject, isString } from '@vue/shared'
1313
import { createComment, createTextNode } from './dom/node'
1414
import {
1515
type Block,
@@ -132,149 +132,173 @@ export const createFor = (
132132
unmount(oldBlocks[i])
133133
}
134134
} else {
135-
let i = 0
136-
let e1 = oldLength - 1 // prev ending index
137-
let e2 = newLength - 1 // next ending index
138-
139-
// 1. sync from start
140-
// (a b) c
141-
// (a b) d e
142-
while (i <= e1 && i <= e2) {
143-
if (tryPatchIndex(source, i)) {
144-
i++
145-
} else {
146-
break
135+
const sharedBlockCount = Math.min(oldLength, newLength)
136+
const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
137+
const queuedBlocks: [
138+
blockIndex: number,
139+
blockItem: ReturnType<typeof getItem>,
140+
blockKey: any,
141+
][] = new Array(newLength)
142+
143+
let anchorFallback: Node = parentAnchor
144+
let endOffset = 0
145+
let startOffset = 0
146+
let queuedBlocksInsertIndex = 0
147+
let previousKeyIndexInsertIndex = 0
148+
149+
while (endOffset < sharedBlockCount) {
150+
const currentIndex = newLength - endOffset - 1
151+
const currentItem = getItem(source, currentIndex)
152+
const currentKey = getKey(...currentItem)
153+
const existingBlock = oldBlocks[oldLength - endOffset - 1]
154+
if (existingBlock.key === currentKey) {
155+
update(existingBlock, ...currentItem)
156+
newBlocks[currentIndex] = existingBlock
157+
endOffset++
158+
continue
159+
}
160+
if (endOffset !== 0) {
161+
anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
147162
}
163+
break
148164
}
149165

150-
// 2. sync from end
151-
// a (b c)
152-
// d e (b c)
153-
while (i <= e1 && i <= e2) {
154-
if (tryPatchIndex(source, i)) {
155-
e1--
156-
e2--
166+
while (startOffset < sharedBlockCount - endOffset) {
167+
const currentItem = getItem(source, startOffset)
168+
const currentKey = getKey(...currentItem)
169+
const previousBlock = oldBlocks[startOffset]
170+
const previousKey = previousBlock.key
171+
if (previousKey === currentKey) {
172+
update((newBlocks[startOffset] = previousBlock), currentItem[0])
157173
} else {
158-
break
174+
queuedBlocks[queuedBlocksInsertIndex++] = [
175+
startOffset,
176+
currentItem,
177+
currentKey,
178+
]
179+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
180+
previousKey,
181+
startOffset,
182+
]
159183
}
184+
startOffset++
160185
}
161186

162-
// 3. common sequence + mount
163-
// (a b)
164-
// (a b) c
165-
// i = 2, e1 = 1, e2 = 2
166-
// (a b)
167-
// c (a b)
168-
// i = 0, e1 = -1, e2 = 0
169-
if (i > e1) {
170-
if (i <= e2) {
171-
const nextPos = e2 + 1
172-
const anchor =
173-
nextPos < newLength
174-
? normalizeAnchor(newBlocks[nextPos].nodes)
175-
: parentAnchor
176-
while (i <= e2) {
177-
mount(source, i, anchor)
178-
i++
179-
}
180-
}
187+
for (let i = startOffset; i < oldLength - endOffset; i++) {
188+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
189+
oldBlocks[i].key,
190+
i,
191+
]
181192
}
182193

183-
// 4. common sequence + unmount
184-
// (a b) c
185-
// (a b)
186-
// i = 2, e1 = 2, e2 = 1
187-
// a (b c)
188-
// (b c)
189-
// i = 0, e1 = 0, e2 = -1
190-
else if (i > e2) {
191-
while (i <= e1) {
192-
unmount(oldBlocks[i])
193-
i++
194-
}
194+
const preparationBlockCount = Math.min(
195+
newLength - endOffset,
196+
sharedBlockCount,
197+
)
198+
for (let i = startOffset; i < preparationBlockCount; i++) {
199+
const blockItem = getItem(source, i)
200+
const blockKey = getKey(...blockItem)
201+
queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
195202
}
196203

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

251-
// 5.3 move and mount
252-
// generate longest stable subsequence only when nodes have moved
253-
const increasingNewIndexSequence = moved
254-
? getSequence(newIndexToOldIndexMap)
255-
: []
256-
j = increasingNewIndexSequence.length - 1
257-
// looping backwards so that we can use last patched node as anchor
258-
for (i = toBePatched - 1; i >= 0; i--) {
259-
const nextIndex = s2 + i
260-
const anchor =
261-
nextIndex + 1 < newLength
262-
? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
263-
: parentAnchor
264-
if (newIndexToOldIndexMap[i] === 0) {
265-
// mount new
266-
mount(source, nextIndex, anchor)
267-
} else if (moved) {
268-
// move if:
269-
// There is no stable subsequence (e.g. a reverse)
270-
// OR current node is not among the stable sequence
271-
if (j < 0 || i !== increasingNewIndexSequence[j]) {
272-
insert(newBlocks[nextIndex].nodes, parent!, anchor)
273-
} else {
274-
j--
275-
}
251+
for (let i = queuedBlocks.length - 1; i >= 0; i--) {
252+
const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
253+
relocateOrMountBlock(
254+
blockIndex,
255+
blockItem,
256+
blockKey,
257+
blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
258+
)
259+
}
260+
261+
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
262+
const blockItem = getItem(source, i)
263+
const blockKey = getKey(...blockItem)
264+
relocateOrMountBlock(i, blockItem, blockKey, -1)
265+
}
266+
267+
const useFastRemove = blocksToMount.length === newLength
268+
269+
for (const leftoverIndex of previousKeyIndexMap.values()) {
270+
unmount(
271+
oldBlocks[leftoverIndex],
272+
!(useFastRemove && canUseFastRemove),
273+
!useFastRemove,
274+
)
275+
}
276+
if (useFastRemove) {
277+
for (const selector of selectors) {
278+
selector.cleanup()
279+
}
280+
if (canUseFastRemove) {
281+
parent!.textContent = ''
282+
parent!.appendChild(parentAnchor)
276283
}
277284
}
285+
286+
for (const [
287+
blockIndex,
288+
blockItem,
289+
blockKey,
290+
anchorOffset,
291+
] of blocksToMount) {
292+
mount(
293+
source,
294+
blockIndex,
295+
anchorOffset === -1
296+
? anchorFallback
297+
: normalizeAnchor(newBlocks[anchorOffset].nodes),
298+
blockItem,
299+
blockKey,
300+
)
301+
}
278302
}
279303
}
280304
}
@@ -294,13 +318,15 @@ export const createFor = (
294318
source: ResolvedSource,
295319
idx: number,
296320
anchor: Node | undefined = parentAnchor,
321+
[item, key, index] = getItem(source, idx),
322+
key2 = getKey && getKey(item, key, index),
297323
): ForBlock => {
298-
const [item, key, index] = getItem(source, idx)
299324
const itemRef = shallowRef(item)
300325
// avoid creating refs if the render fn doesn't need it
301326
const keyRef = needKey ? shallowRef(key) : undefined
302327
const indexRef = needIndex ? shallowRef(index) : undefined
303328

329+
currentKey = key2
304330
let nodes: Block
305331
let scope: EffectScope | undefined
306332
if (isComponent) {
@@ -319,23 +345,14 @@ export const createFor = (
319345
itemRef,
320346
keyRef,
321347
indexRef,
322-
getKey && getKey(item, key, index),
348+
key2,
323349
))
324350

325351
if (parent) insert(block.nodes, parent, anchor)
326352

327353
return block
328354
}
329355

330-
const tryPatchIndex = (source: any, idx: number) => {
331-
const block = oldBlocks[idx]
332-
const [item, key, index] = getItem(source, idx)
333-
if (block.key === getKey!(item, key, index)) {
334-
update((newBlocks[idx] = block), item)
335-
return true
336-
}
337-
}
338-
339356
const update = (
340357
{ itemRef, keyRef, indexRef }: ForBlock,
341358
newItem: any,

0 commit comments

Comments
 (0)