@@ -13,7 +13,13 @@ import {
1313 toReadonly ,
1414 watch ,
1515} from '@vue/reactivity'
16- import { getSequence , isArray , isObject , isString } from '@vue/shared'
16+ import {
17+ EMPTY_ARR ,
18+ getSequence ,
19+ isArray ,
20+ isObject ,
21+ isString ,
22+ } from '@vue/shared'
1723import { createComment , createTextNode } from './dom/node'
1824import {
1925 type Block ,
@@ -181,9 +187,10 @@ export const createFor = (
181187 if ( getKey ) {
182188 newKeys = new Array ( newLength )
183189 for ( let i = 0 ; i < newLength ; i ++ ) {
190+ const value = getItemValue ( source , i )
184191 newKeys [ i ] = sourceKeys
185- ? getKey ( getItemValue ( source , i ) , sourceKeys [ i ] , i )
186- : getKey ( getItemValue ( source , i ) , i , undefined )
192+ ? getKey ( value , sourceKeys [ i ] , i )
193+ : getKey ( value , i , undefined )
187194 }
188195 }
189196
@@ -275,15 +282,11 @@ export const createFor = (
275282 const queuedIndices : number [ ] = [ ]
276283 const oldKeyIndexMap = new Map < any , number > ( )
277284
278- // in-place matches, remembered so a dense reorder can pull them into
279- // the move plan (they sit at the same index in both lists)
280- let stationaryIndices : number [ ] | undefined
281285 for ( let i = 0 ; i < e1 ; i ++ ) {
282286 const currentKey = newKeys ! [ i ]
283287 const oldBlock = oldBlocks [ i ]
284288 if ( oldBlock . key === currentKey ) {
285289 updateAt ( ( newBlocks [ i ] = oldBlock ) , source , i )
286- ; ( stationaryIndices ||= [ ] ) . push ( i )
287290 } else {
288291 queuedIndices . push ( i )
289292 oldKeyIndexMap . set ( oldBlock . key , i )
@@ -304,16 +307,16 @@ export const createFor = (
304307 // marks a fresh mount). The bounds filter below zeroes reuses that may
305308 // not stay put; the apply pass tells move from mount by
306309 // `newBlocks[index]`, which the reuse pass has already filled in.
307- const sources : number [ ] = new Array ( queuedLength )
310+ let sources : number [ ] = EMPTY_ARR as unknown as number [ ]
308311 let mountCounter = 0
309312
310313 if ( oldKeyIndexMap . size === 0 ) {
311- // pure append/replace: nothing to pair up. Plain loop over fill():
312- // the inlined monomorphic store beats the generic builtin on a
313- // freshly allocated (holey) array.
314- for ( let q = 0 ; q < queuedLength ; q ++ ) sources [ q ] = 0
314+ // pure append/replace: nothing to pair up, so every queued index is
315+ // a mount, the planner below is skipped and `sources` is never
316+ // read - don't build it
315317 mountCounter = queuedLength
316318 } else {
319+ sources = new Array ( queuedLength )
317320 for ( let q = queuedLength - 1 ; q >= 0 ; q -- ) {
318321 const index = queuedIndices [ q ]
319322 const key = newKeys ! [ index ]
@@ -337,114 +340,28 @@ export const createFor = (
337340 if ( useFastRemove && frag . resetListeners ) {
338341 for ( const fn of frag . resetListeners ) fn ( )
339342 }
340- for ( const leftoverIndex of oldKeyIndexMap . values ( ) ) {
341- unmount (
342- oldBlocks [ leftoverIndex ] ,
343- ! ( useFastRemove && canUseFastRemove ) ,
344- )
343+ if ( oldKeyIndexMap . size ) {
344+ for ( const leftoverIndex of oldKeyIndexMap . values ( ) ) {
345+ unmount (
346+ oldBlocks [ leftoverIndex ] ,
347+ ! ( useFastRemove && canUseFastRemove ) ,
348+ )
349+ }
345350 }
346351 if ( useFastRemove && canUseFastRemove ) {
347352 parent ! . textContent = ''
348353 parent ! . appendChild ( parentAnchor )
349354 }
350355
351- // Decide which reused blocks may stay in place: the longest
352- // increasing subsequence of old indices is already in relative order,
353- // so only the blocks outside it need a DOM move.
354- //
355- // Two planners, picked by how dense the change is across the range it
356- // touches:
357- //
358- // - dense (the changed indices cover at least half their own span):
359- // pull the in-place matches inside that span into the plan too and
360- // run an unbounded LIS, which is move-count minimal like vdom's.
361- // `span <= 2 * queuedLength` here, so this stays O(queued).
362- // - sparse (a couple of rows moved across an otherwise untouched
363- // list, e.g. a far swap): keep the in-place matches pinned, which
364- // costs nothing but bounds each segment's LIS by its stationary
365- // neighbours. Not minimal in general, but it never walks the
366- // untouched majority.
367356 let sequence : number [ ] | undefined
368- let allKept = false
369- if ( mountCounter !== queuedLength ) {
370- const span = queuedIndices [ queuedLength - 1 ] - queuedIndices [ 0 ] + 1
371- if ( stationaryIndices && queuedLength * 2 >= span ) {
372- // dense: merge the stationaries inside the span into the plan,
373- // keeping both parallel arrays in ascending index order
374- const firstIndex = queuedIndices [ 0 ]
375- const lastIndex = queuedIndices [ queuedLength - 1 ]
376- for ( let i = 0 ; i < stationaryIndices . length ; i ++ ) {
377- const index = stationaryIndices [ i ]
378- if ( index > firstIndex && index < lastIndex ) {
379- queuedIndices . push ( index )
380- // a prefix stationary sits at the same index in both lists
381- sources . push ( index + 1 )
382- }
383- }
384- if ( queuedIndices . length !== queuedLength ) {
385- sortQueueByIndex ( queuedIndices , sources )
386- queuedLength = queuedIndices . length
387- }
388- }
389-
390- let eligible = 0
391- // if the eligible old positions already ascend, they are all in
392- // relative order and every one of them stays put — no LIS needed
393- let moved = false
394- let maxSource = 0
395- if ( queuedLength * 2 >= span ) {
396- // dense: no bounds, every reuse competes for the LIS
397- for ( let q = 0 ; q < queuedLength ; q ++ ) {
398- const value = sources [ q ]
399- if ( value !== 0 ) {
400- eligible ++
401- if ( value < maxSource ) moved = true
402- else maxSource = value
403- }
404- }
405- } else {
406- // sparse: in-place matches partition the queued indices into
407- // segments of consecutive positions, and bound each segment's
408- // eligible old indices. Because those bounds never overlap
409- // between segments, one whole-array LIS still yields the same
410- // answer as running it per segment.
411- let seg = 0
412- while ( seg < queuedLength ) {
413- let segEnd = seg
414- while (
415- segEnd + 1 < queuedLength &&
416- queuedIndices [ segEnd + 1 ] === queuedIndices [ segEnd ] + 1
417- ) {
418- segEnd ++
419- }
420- // prefix stationaries sit at their own index in both lists; the
421- // first suffix stationary (index e3) sits at old index e2. With
422- // no bounding stationary the bounds are -1 / e2 == oldLength.
423- const lowerBound = queuedIndices [ seg ] - 1
424- const nextIndex = queuedIndices [ segEnd ] + 1
425- const upperBound = nextIndex < e3 ? nextIndex : e2
426- for ( let q = seg ; q <= segEnd ; q ++ ) {
427- const value = sources [ q ]
428- const oldIndex = value - 1
429- if (
430- oldIndex < 0 ||
431- oldIndex <= lowerBound ||
432- oldIndex >= upperBound
433- ) {
434- sources [ q ] = 0
435- } else {
436- eligible ++
437- if ( value < maxSource ) moved = true
438- else maxSource = value
439- }
440- }
441- seg = segEnd + 1
442- }
443- }
444-
445- if ( eligible && moved ) sequence = getSequence ( sources )
446- else if ( eligible ) allKept = true
357+ const hasReuse = mountCounter !== queuedLength
358+ if ( hasReuse ) {
359+ sequence = planMoves ( queuedIndices , sources , e2 , e3 )
360+ // the dense planner expands both arrays in place
361+ queuedLength = queuedIndices . length
447362 }
363+ // reuses exist and none of them moved: every one of them stays put
364+ const allKept = hasReuse && sequence === undefined
448365
449366 // apply back-to-front so every block can anchor on the finalized
450367 // block after it (kept blocks count as finalized: relative order
@@ -464,6 +381,10 @@ export const createFor = (
464381 // a leading entry that was never selected; skip that marker
465382 isKept = sources [ q ] !== 0
466383 }
384+ // `scanFrom`/`cachedAnchor` stay where they are: the next block
385+ // that does move rescans the wider range, and each position is
386+ // still visited at most once.
387+ if ( isKept ) continue
467388
468389 // A block that renders nothing has no first node, so look past it
469390 // for the next attached one.
@@ -504,20 +425,22 @@ export const createFor = (
504425 scanFrom = index + 1
505426 cachedAnchor = anchorNode
506427
507- if ( isKept ) continue
508- if ( newBlocks [ index ] !== undefined ) {
509- insertForBlock ( newBlocks [ index ] , anchorNode )
428+ const block = newBlocks [ index ]
429+ if ( block !== undefined ) {
430+ insertForBlock ( block , anchorNode )
510431 } else {
511432 mount ( source , index , anchorNode )
512433 }
513434 }
514435 }
515436 }
516437
517- frag . nodes = [ ( oldBlocks = newBlocks ) ]
518- if ( parentAnchor ) frag . nodes . push ( parentAnchor )
438+ oldBlocks = newBlocks
439+ frag . nodes = parentAnchor ? [ newBlocks , parentAnchor ] : [ newBlocks ]
519440
520- if ( wasMounted && frag . onUpdated ) frag . onUpdated . forEach ( m => m ( ) )
441+ if ( wasMounted && frag . onUpdated ) {
442+ for ( const fn of frag . onUpdated ) fn ( )
443+ }
521444 setActiveSub ( prevSub )
522445 }
523446
@@ -627,9 +550,7 @@ export const createFor = (
627550 try {
628551 if ( emptyLocalRange && newLength ) {
629552 reuseBoundaryClose ( hydrationStart )
630- for ( let i = 0 ; i < newLength ; i ++ ) {
631- mount ( source , i , parentAnchor )
632- }
553+ mountAll ( source , newLength )
633554 setCurrentHydrationNode ( parentAnchor )
634555 } else {
635556 for ( let i = 0 ; i < newLength ; i ++ ) {
@@ -877,22 +798,112 @@ export function createSelector(source: () => any): ForSelector {
877798 return register
878799}
879800
880- // Restores ascending index order after the dense planner appended the
881- // stationaries; both arrays move together. The appended tail is itself sorted
882- // and usually short, so insertion sort runs near-linearly here.
883- function sortQueueByIndex ( indices : number [ ] , oldIndices : number [ ] ) : void {
884- for ( let i = 1 ; i < indices . length ; i ++ ) {
885- const index = indices [ i ]
886- const oldIndex = oldIndices [ i ]
887- let j = i - 1
888- while ( j >= 0 && indices [ j ] > index ) {
889- indices [ j + 1 ] = indices [ j ]
890- oldIndices [ j + 1 ] = oldIndices [ j ]
891- j --
801+ /**
802+ * Decides which reused blocks may stay where they are: the longest increasing
803+ * subsequence of their old indices is already in relative order, so only the
804+ * blocks outside it need a DOM move. Returns that subsequence as indices into
805+ * `sources`, or `undefined` when no reused block has to move at all.
806+ *
807+ * Two planners, picked by how dense the change is across the range it touches:
808+ *
809+ * - dense (the queued indices cover at least half of the range they span):
810+ * pull the in-place matches in that range into the plan too and run an
811+ * unbounded LIS, which is move-count minimal like vdom's. The range is at
812+ * most `2 * queuedIndices.length` wide here, so this stays O(queued).
813+ * - sparse (a couple of rows moved across an otherwise untouched list, e.g. a
814+ * far swap): keep the in-place matches pinned, which costs nothing but
815+ * bounds each segment's LIS by its stationary neighbours. Not minimal in
816+ * general, but it never walks the untouched majority.
817+ *
818+ * Both arrays are expanded in place on the dense path.
819+ */
820+ function planMoves (
821+ queuedIndices : number [ ] ,
822+ sources : number [ ] ,
823+ e2 : number ,
824+ e3 : number ,
825+ ) : number [ ] | undefined {
826+ let queuedLength = queuedIndices . length
827+ // The dense range runs from the first queued index to the start of the
828+ // synchronized suffix: an in-place match on either side of a queued block
829+ // still constrains where it may land, so the plan has to cover them all,
830+ // not just the ones between the first and last queued index.
831+ const firstIndex = queuedIndices [ 0 ]
832+ const denseLength = e3 - firstIndex
833+ const isDense = queuedLength * 2 >= denseLength
834+
835+ // if the eligible old positions already ascend, they are all in relative
836+ // order and every one of them stays put - no LIS needed
837+ let moved = false
838+ let maxSource = 0
839+
840+ if ( isDense ) {
841+ // Expand in place, back to front so the tail writes never clobber entries
842+ // still to be read. Every position in the range that is not queued is a
843+ // same-index in-place match, so it needs no lookup.
844+ let read = queuedLength - 1
845+ queuedIndices . length = sources . length = denseLength
846+ for (
847+ let write = denseLength - 1 , index = e3 - 1 ;
848+ write >= 0 ;
849+ write -- , index --
850+ ) {
851+ if ( read >= 0 && queuedIndices [ read ] === index ) {
852+ sources [ write ] = sources [ read -- ]
853+ } else {
854+ sources [ write ] = index + 1
855+ }
856+ queuedIndices [ write ] = index
857+ }
858+ queuedLength = denseLength
859+
860+ // no bounds: every reuse competes for the LIS
861+ for ( let q = 0 ; q < queuedLength ; q ++ ) {
862+ const value = sources [ q ]
863+ if ( value !== 0 ) {
864+ if ( value < maxSource ) moved = true
865+ else maxSource = value
866+ }
867+ }
868+ } else {
869+ // in-place matches partition the queued indices into segments of
870+ // consecutive positions, and bound each segment's eligible old indices.
871+ // Because those bounds never overlap between segments, one whole-array
872+ // LIS still yields the same answer as running it per segment.
873+ let seg = 0
874+ while ( seg < queuedLength ) {
875+ let segEnd = seg
876+ while (
877+ segEnd + 1 < queuedLength &&
878+ queuedIndices [ segEnd + 1 ] === queuedIndices [ segEnd ] + 1
879+ ) {
880+ segEnd ++
881+ }
882+ // prefix stationaries sit at their own index in both lists; the first
883+ // suffix stationary (index e3) sits at old index e2. With no bounding
884+ // stationary the bounds are -1 / e2 == oldLength.
885+ const lowerBound = queuedIndices [ seg ] - 1
886+ const nextIndex = queuedIndices [ segEnd ] + 1
887+ const upperBound = nextIndex < e3 ? nextIndex : e2
888+ for ( let q = seg ; q <= segEnd ; q ++ ) {
889+ const value = sources [ q ]
890+ // a mount has value 0, i.e. an old index of -1, which the lower
891+ // bound (>= -1) already rejects
892+ const oldIndex = value - 1
893+ if ( oldIndex <= lowerBound || oldIndex >= upperBound ) {
894+ sources [ q ] = 0
895+ } else {
896+ if ( value < maxSource ) moved = true
897+ else maxSource = value
898+ }
899+ }
900+ seg = segEnd + 1
892901 }
893- indices [ j + 1 ] = index
894- oldIndices [ j + 1 ] = oldIndex
895902 }
903+
904+ // `moved` can only be set by the second non-zero value onwards, so it
905+ // already implies there is something to keep
906+ return moved ? getSequence ( sources ) : undefined
896907}
897908
898909function stopBlockScopes ( blocks : ForBlock [ ] ) : void {
0 commit comments