@@ -18,7 +18,7 @@ import type {
1818 ItemResize ,
1919 ItemsRange ,
2020} from "./types" ;
21- import { abs , max , min , NULL } from "./utils" ;
21+ import { abs , max , microtask , min , NULL } from "./utils" ;
2222
2323const MAX_INT_32 = 0x7fffffff ;
2424
@@ -55,18 +55,21 @@ export const ACTION_MANUAL_SCROLL = 7;
5555/** @internal */
5656export const ACTION_BEFORE_MANUAL_SMOOTH_SCROLL = 8 ;
5757
58- type Actions =
59- | [ type : typeof ACTION_SCROLL , offset : number ]
60- | [ type : typeof ACTION_SCROLL_END , dummy ?: void ]
61- | [ type : typeof ACTION_ITEM_RESIZE , entries : ItemResize [ ] ]
62- | [ type : typeof ACTION_VIEWPORT_RESIZE , size : number ]
63- | [
64- type : typeof ACTION_ITEMS_LENGTH_CHANGE ,
65- arg : [ length : number , isShift ?: boolean | undefined ] ,
66- ]
67- | [ type : typeof ACTION_START_OFFSET_CHANGE , offset : number ]
68- | [ type : typeof ACTION_MANUAL_SCROLL , dummy ?: void ]
69- | [ type : typeof ACTION_BEFORE_MANUAL_SMOOTH_SCROLL , offset : number ] ;
58+ type Actions = [
59+ ...args :
60+ | [ type : typeof ACTION_SCROLL , offset : number ]
61+ | [ type : typeof ACTION_SCROLL_END , dummy ?: void ]
62+ | [ type : typeof ACTION_ITEM_RESIZE , entry : ItemResize ]
63+ | [ type : typeof ACTION_VIEWPORT_RESIZE , size : number ]
64+ | [
65+ type : typeof ACTION_ITEMS_LENGTH_CHANGE ,
66+ arg : [ length : number , isShift ?: boolean | undefined ]
67+ ]
68+ | [ type : typeof ACTION_START_OFFSET_CHANGE , offset : number ]
69+ | [ type : typeof ACTION_MANUAL_SCROLL , dummy ?: void ]
70+ | [ type : typeof ACTION_BEFORE_MANUAL_SMOOTH_SCROLL , offset : number ] ,
71+ immediate ?: boolean
72+ ] ;
7073
7174/** @internal */
7275export const UPDATE_VIRTUAL_STATE = 0b0001 ;
@@ -133,7 +136,10 @@ export const createVirtualStore = (
133136 shouldAutoEstimateItemSize : boolean = false
134137) : VirtualStore => {
135138 let isSSR = ! ! ssrCount ;
139+ let isRerenderScheduled = false ;
140+ let shouldSync = false ;
136141 let stateVersion : StateVersion = 1 ;
142+ let updatedBit = 0 ;
137143 let viewportSize = 0 ;
138144 let startSpacerSize = 0 ;
139145 let scrollOffset = 0 ;
@@ -246,10 +252,8 @@ export const createVirtualStore = (
246252 subscribers . delete ( sub ) ;
247253 } ;
248254 } ,
249- $update : ( type , payload ) : void => {
255+ $update : ( type , payload , immediate = false ) => {
250256 let shouldFlushPendingJump : boolean | undefined ;
251- let shouldSync : boolean | undefined ;
252- let mutated = 0 ;
253257
254258 switch ( type ) {
255259 case ACTION_SCROLL : {
@@ -291,7 +295,7 @@ export const createVirtualStore = (
291295 }
292296
293297 scrollOffset = payload ;
294- mutated = UPDATE_SCROLL_EVENT ;
298+ updatedBit | = UPDATE_SCROLL_EVENT ;
295299
296300 // Skip if offset is not changed
297301 // Scroll offset may exceed min or max especially in Safari's elastic scrolling.
@@ -300,69 +304,59 @@ export const createVirtualStore = (
300304 relativeOffset >= - viewportSize &&
301305 relativeOffset <= getTotalSize ( )
302306 ) {
303- mutated + = UPDATE_VIRTUAL_STATE ;
307+ updatedBit | = UPDATE_VIRTUAL_STATE ;
304308
305309 // Update synchronously if scrolled a lot
306310 shouldSync = distance > viewportSize ;
307311 }
308312 break ;
309313 }
310314 case ACTION_SCROLL_END : {
311- mutated = UPDATE_SCROLL_END_EVENT ;
315+ updatedBit | = UPDATE_SCROLL_END_EVENT ;
312316 if ( _scrollDirection !== SCROLL_IDLE ) {
313317 shouldFlushPendingJump = true ;
314- mutated + = UPDATE_VIRTUAL_STATE ;
318+ updatedBit | = UPDATE_VIRTUAL_STATE ;
315319 }
316320 _scrollDirection = SCROLL_IDLE ;
317321 _scrollMode = SCROLL_BY_NATIVE ;
318322 _frozenRange = NULL ;
319323 break ;
320324 }
321325 case ACTION_ITEM_RESIZE : {
322- const updated = payload . filter (
323- ( [ index , size ] ) => cache . _sizes [ index ] !== size
324- ) ;
326+ const [ index , size ] = payload ;
325327
326- // Skip if all items are cached and not updated
327- if ( ! updated . length ) {
328+ // Skip if item is cached and not updated
329+ if ( cache . _sizes [ index ] === size ) {
328330 break ;
329331 }
330332
333+ const prevSize = getItemSize ( index ) ;
334+
331335 // Calculate jump by resize to minimize junks in appearance
332- applyJump (
333- updated . reduce ( ( acc , [ index , size ] ) => {
334- if (
335- // Keep distance from end during shifting
336- _scrollMode === SCROLL_BY_SHIFT ||
337- ( _frozenRange
338- ? // https://github.com/inokawa/virtua/issues/380
339- // https://github.com/inokawa/virtua/issues/590
340- ! isSSR && index < _frozenRange [ 0 ]
341- : // Otherwise we should maintain visible position
342- getItemOffset ( index ) +
343- // https://github.com/inokawa/virtua/issues/385
344- ( _scrollDirection === SCROLL_IDLE &&
345- _scrollMode === SCROLL_BY_NATIVE
346- ? getItemSize ( index )
347- : 0 ) <
348- getRelativeScrollOffset ( ) )
349- ) {
350- acc += size - getItemSize ( index ) ;
351- }
352- return acc ;
353- } , 0 )
354- ) ;
355-
356- // Update item sizes
357- for ( const [ index , size ] of updated ) {
358- const prevSize = getItemSize ( index ) ;
359- const isInitialMeasurement = setItemSize ( cache , index , size ) ;
360-
361- if ( shouldAutoEstimateItemSize ) {
362- _totalMeasuredSize += isInitialMeasurement
363- ? size
364- : size - prevSize ;
365- }
336+ if (
337+ // Keep distance from end during shifting
338+ _scrollMode === SCROLL_BY_SHIFT ||
339+ ( _frozenRange
340+ ? // https://github.com/inokawa/virtua/issues/380
341+ // https://github.com/inokawa/virtua/issues/590
342+ ! isSSR && index < _frozenRange [ 0 ]
343+ : // Otherwise we should maintain visible position
344+ getItemOffset ( index ) +
345+ // https://github.com/inokawa/virtua/issues/385
346+ ( _scrollDirection === SCROLL_IDLE &&
347+ _scrollMode === SCROLL_BY_NATIVE
348+ ? prevSize
349+ : 0 ) <
350+ getRelativeScrollOffset ( ) )
351+ ) {
352+ applyJump ( size - prevSize ) ;
353+ }
354+
355+ // Update item size
356+ const isInitialMeasurement = setItemSize ( cache , index , size ) ;
357+
358+ if ( shouldAutoEstimateItemSize ) {
359+ _totalMeasuredSize += isInitialMeasurement ? size : size - prevSize ;
366360 }
367361
368362 // Estimate initial item size from measured sizes
@@ -381,7 +375,7 @@ export const createVirtualStore = (
381375 shouldAutoEstimateItemSize = false ;
382376 }
383377
384- mutated = UPDATE_VIRTUAL_STATE + UPDATE_SIZE_EVENT ;
378+ updatedBit | = UPDATE_VIRTUAL_STATE | UPDATE_SIZE_EVENT ;
385379
386380 // Synchronous update is necessary in current design to minimize visible glitch in concurrent rendering.
387381 // However this seems to be the main cause of the errors from ResizeObserver.
@@ -395,20 +389,20 @@ export const createVirtualStore = (
395389 case ACTION_VIEWPORT_RESIZE : {
396390 if ( viewportSize !== payload ) {
397391 viewportSize = payload ;
398- mutated = UPDATE_VIRTUAL_STATE + UPDATE_SIZE_EVENT ;
392+ updatedBit | = UPDATE_VIRTUAL_STATE | UPDATE_SIZE_EVENT ;
399393 }
400394 break ;
401395 }
402396 case ACTION_ITEMS_LENGTH_CHANGE : {
403397 if ( payload [ 1 ] ) {
404398 applyJump ( updateCacheLength ( cache , payload [ 0 ] , true ) ) ;
405399 _scrollMode = SCROLL_BY_SHIFT ;
406- mutated = UPDATE_VIRTUAL_STATE ;
400+ updatedBit | = UPDATE_VIRTUAL_STATE ;
407401 } else {
408402 updateCacheLength ( cache , payload [ 0 ] ) ;
409403 // https://github.com/inokawa/virtua/issues/552
410404 // https://github.com/inokawa/virtua/issues/557
411- mutated = UPDATE_VIRTUAL_STATE ;
405+ updatedBit | = UPDATE_VIRTUAL_STATE ;
412406 }
413407 break ;
414408 }
@@ -422,28 +416,43 @@ export const createVirtualStore = (
422416 }
423417 case ACTION_BEFORE_MANUAL_SMOOTH_SCROLL : {
424418 _frozenRange = getRange ( payload ) ;
425- mutated = UPDATE_VIRTUAL_STATE ;
419+ updatedBit | = UPDATE_VIRTUAL_STATE ;
426420 break ;
427421 }
428422 }
429423
430- if ( mutated ) {
431- stateVersion = ( stateVersion & MAX_INT_32 ) + 1 ;
432-
424+ if ( updatedBit ) {
433425 if ( shouldFlushPendingJump && pendingJump ) {
434426 jump += pendingJump ;
435427 pendingJump = 0 ;
436428 }
437429
438- subscribers . forEach ( ( [ target , cb ] ) => {
439- // Early return to skip React's computation
440- if ( ! ( mutated & target ) ) {
441- return ;
430+ if ( ! isRerenderScheduled ) {
431+ isRerenderScheduled = true ;
432+ const flush = ( ) => {
433+ const _updatedBit = updatedBit ;
434+ const _shouldSync = shouldSync ;
435+ updatedBit = 0 ;
436+ isRerenderScheduled = shouldSync = false ;
437+
438+ stateVersion = ( stateVersion & MAX_INT_32 ) + 1 ;
439+
440+ subscribers . forEach ( ( [ target , cb ] ) => {
441+ // Early return to skip React's computation
442+ if ( ! ( _updatedBit & target ) ) {
443+ return ;
444+ }
445+ // https://github.com/facebook/react/issues/25191
446+ // https://github.com/facebook/react/blob/a5fc797db14c6e05d4d5c4dbb22a0dd70d41f5d5/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1443-L1447
447+ cb ( _shouldSync ) ;
448+ } ) ;
449+ } ;
450+ if ( immediate ) {
451+ flush ( ) ;
452+ } else {
453+ microtask ( flush ) ;
442454 }
443- // https://github.com/facebook/react/issues/25191
444- // https://github.com/facebook/react/blob/a5fc797db14c6e05d4d5c4dbb22a0dd70d41f5d5/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1443-L1447
445- cb ( shouldSync ) ;
446- } ) ;
455+ }
447456 }
448457 } ,
449458 } ;
0 commit comments