Skip to content

Commit 7cf9d98

Browse files
perf(vapor): more efficient renderList update algorithm (#13279)
1 parent d5adf95 commit 7cf9d98

File tree

1 file changed

+154
-137
lines changed

1 file changed

+154
-137
lines changed

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 154 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
toReadonly,
1212
watch,
1313
} from '@vue/reactivity'
14-
import { getSequence, isArray, isObject, isString } from '@vue/shared'
14+
import { isArray, isObject, isString } from '@vue/shared'
1515
import { createComment, createTextNode } from './dom/node'
1616
import {
1717
type Block,
@@ -150,149 +150,173 @@ export const createFor = (
150150
unmount(oldBlocks[i])
151151
}
152152
} else {
153-
let i = 0
154-
let e1 = oldLength - 1 // prev ending index
155-
let e2 = newLength - 1 // next ending index
156-
157-
// 1. sync from start
158-
// (a b) c
159-
// (a b) d e
160-
while (i <= e1 && i <= e2) {
161-
if (tryPatchIndex(source, i)) {
162-
i++
163-
} else {
164-
break
153+
const sharedBlockCount = Math.min(oldLength, newLength)
154+
const previousKeyIndexPairs: [any, number][] = new Array(oldLength)
155+
const queuedBlocks: [
156+
blockIndex: number,
157+
blockItem: ReturnType<typeof getItem>,
158+
blockKey: any,
159+
][] = new Array(newLength)
160+
161+
let anchorFallback: Node = parentAnchor
162+
let endOffset = 0
163+
let startOffset = 0
164+
let queuedBlocksInsertIndex = 0
165+
let previousKeyIndexInsertIndex = 0
166+
167+
while (endOffset < sharedBlockCount) {
168+
const currentIndex = newLength - endOffset - 1
169+
const currentItem = getItem(source, currentIndex)
170+
const currentKey = getKey(...currentItem)
171+
const existingBlock = oldBlocks[oldLength - endOffset - 1]
172+
if (existingBlock.key === currentKey) {
173+
update(existingBlock, ...currentItem)
174+
newBlocks[currentIndex] = existingBlock
175+
endOffset++
176+
continue
177+
}
178+
if (endOffset !== 0) {
179+
anchorFallback = normalizeAnchor(newBlocks[currentIndex + 1].nodes)
165180
}
181+
break
166182
}
167183

168-
// 2. sync from end
169-
// a (b c)
170-
// d e (b c)
171-
while (i <= e1 && i <= e2) {
172-
if (tryPatchIndex(source, i)) {
173-
e1--
174-
e2--
184+
while (startOffset < sharedBlockCount - endOffset) {
185+
const currentItem = getItem(source, startOffset)
186+
const currentKey = getKey(...currentItem)
187+
const previousBlock = oldBlocks[startOffset]
188+
const previousKey = previousBlock.key
189+
if (previousKey === currentKey) {
190+
update((newBlocks[startOffset] = previousBlock), currentItem[0])
175191
} else {
176-
break
192+
queuedBlocks[queuedBlocksInsertIndex++] = [
193+
startOffset,
194+
currentItem,
195+
currentKey,
196+
]
197+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
198+
previousKey,
199+
startOffset,
200+
]
177201
}
202+
startOffset++
178203
}
179204

180-
// 3. common sequence + mount
181-
// (a b)
182-
// (a b) c
183-
// i = 2, e1 = 1, e2 = 2
184-
// (a b)
185-
// c (a b)
186-
// i = 0, e1 = -1, e2 = 0
187-
if (i > e1) {
188-
if (i <= e2) {
189-
const nextPos = e2 + 1
190-
const anchor =
191-
nextPos < newLength
192-
? normalizeAnchor(newBlocks[nextPos].nodes)
193-
: parentAnchor
194-
while (i <= e2) {
195-
mount(source, i, anchor)
196-
i++
197-
}
198-
}
205+
for (let i = startOffset; i < oldLength - endOffset; i++) {
206+
previousKeyIndexPairs[previousKeyIndexInsertIndex++] = [
207+
oldBlocks[i].key,
208+
i,
209+
]
199210
}
200211

201-
// 4. common sequence + unmount
202-
// (a b) c
203-
// (a b)
204-
// i = 2, e1 = 2, e2 = 1
205-
// a (b c)
206-
// (b c)
207-
// i = 0, e1 = 0, e2 = -1
208-
else if (i > e2) {
209-
while (i <= e1) {
210-
unmount(oldBlocks[i])
211-
i++
212-
}
212+
const preparationBlockCount = Math.min(
213+
newLength - endOffset,
214+
sharedBlockCount,
215+
)
216+
for (let i = startOffset; i < preparationBlockCount; i++) {
217+
const blockItem = getItem(source, i)
218+
const blockKey = getKey(...blockItem)
219+
queuedBlocks[queuedBlocksInsertIndex++] = [i, blockItem, blockKey]
213220
}
214221

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

269-
// 5.3 move and mount
270-
// generate longest stable subsequence only when nodes have moved
271-
const increasingNewIndexSequence = moved
272-
? getSequence(newIndexToOldIndexMap)
273-
: []
274-
j = increasingNewIndexSequence.length - 1
275-
// looping backwards so that we can use last patched node as anchor
276-
for (i = toBePatched - 1; i >= 0; i--) {
277-
const nextIndex = s2 + i
278-
const anchor =
279-
nextIndex + 1 < newLength
280-
? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
281-
: parentAnchor
282-
if (newIndexToOldIndexMap[i] === 0) {
283-
// mount new
284-
mount(source, nextIndex, anchor)
285-
} else if (moved) {
286-
// move if:
287-
// There is no stable subsequence (e.g. a reverse)
288-
// OR current node is not among the stable sequence
289-
if (j < 0 || i !== increasingNewIndexSequence[j]) {
290-
insert(newBlocks[nextIndex].nodes, parent!, anchor)
291-
} else {
292-
j--
293-
}
269+
for (let i = queuedBlocks.length - 1; i >= 0; i--) {
270+
const [blockIndex, blockItem, blockKey] = queuedBlocks[i]
271+
relocateOrMountBlock(
272+
blockIndex,
273+
blockItem,
274+
blockKey,
275+
blockIndex < preparationBlockCount - 1 ? blockIndex + 1 : -1,
276+
)
277+
}
278+
279+
for (let i = preparationBlockCount; i < newLength - endOffset; i++) {
280+
const blockItem = getItem(source, i)
281+
const blockKey = getKey(...blockItem)
282+
relocateOrMountBlock(i, blockItem, blockKey, -1)
283+
}
284+
285+
const useFastRemove = blocksToMount.length === newLength
286+
287+
for (const leftoverIndex of previousKeyIndexMap.values()) {
288+
unmount(
289+
oldBlocks[leftoverIndex],
290+
!(useFastRemove && canUseFastRemove),
291+
!useFastRemove,
292+
)
293+
}
294+
if (useFastRemove) {
295+
for (const selector of selectors) {
296+
selector.cleanup()
297+
}
298+
if (canUseFastRemove) {
299+
parent!.textContent = ''
300+
parent!.appendChild(parentAnchor)
294301
}
295302
}
303+
304+
for (const [
305+
blockIndex,
306+
blockItem,
307+
blockKey,
308+
anchorOffset,
309+
] of blocksToMount) {
310+
mount(
311+
source,
312+
blockIndex,
313+
anchorOffset === -1
314+
? anchorFallback
315+
: normalizeAnchor(newBlocks[anchorOffset].nodes),
316+
blockItem,
317+
blockKey,
318+
)
319+
}
296320
}
297321
}
298322
}
@@ -312,13 +336,15 @@ export const createFor = (
312336
source: ResolvedSource,
313337
idx: number,
314338
anchor: Node | undefined = parentAnchor,
339+
[item, key, index] = getItem(source, idx),
340+
key2 = getKey && getKey(item, key, index),
315341
): ForBlock => {
316-
const [item, key, index] = getItem(source, idx)
317342
const itemRef = shallowRef(item)
318343
// avoid creating refs if the render fn doesn't need it
319344
const keyRef = needKey ? shallowRef(key) : undefined
320345
const indexRef = needIndex ? shallowRef(index) : undefined
321346

347+
currentKey = key2
322348
let nodes: Block
323349
let scope: EffectScope | undefined
324350
if (isComponent) {
@@ -337,23 +363,14 @@ export const createFor = (
337363
itemRef,
338364
keyRef,
339365
indexRef,
340-
getKey && getKey(item, key, index),
366+
key2,
341367
))
342368

343369
if (parent) insert(block.nodes, parent, anchor)
344370

345371
return block
346372
}
347373

348-
const tryPatchIndex = (source: any, idx: number) => {
349-
const block = oldBlocks[idx]
350-
const [item, key, index] = getItem(source, idx)
351-
if (block.key === getKey!(item, key, index)) {
352-
update((newBlocks[idx] = block), item)
353-
return true
354-
}
355-
}
356-
357374
const update = (
358375
{ itemRef, keyRef, indexRef }: ForBlock,
359376
newItem: any,

0 commit comments

Comments
 (0)