3232 */
3333'use strict' ;
3434
35+ const Batchinator = require ( 'Batchinator' ) ;
3536const IncrementalGroup = require ( 'IncrementalGroup' ) ;
3637const React = require ( 'React' ) ;
3738const ScrollView = require ( 'ScrollView' ) ;
@@ -149,13 +150,14 @@ type Props = {
149150 */
150151 recomputeRowsBatchingPeriod : number ,
151152 /**
152- * Called when rows will be mounted/unmounted. Mounted rows always form a contiguous block so it is expressed as a
153- * range of start plus count.
153+ * Called when rows will be mounted/unmounted. Mounted rows always form a contiguous block so it
154+ * is expressed as a range of start plus count.
154155 */
155156 onMountedRowsWillChange ?: ( firstIdx : number , count : number ) => void ,
156157 /**
157- * Change this when you want to make sure the WindowedListView will re-render, for example when the result of
158- * `renderScrollComponent` might change. It will be compared in `shouldComponentUpdate`.
158+ * Change this when you want to make sure the WindowedListView will re-render, for example when
159+ * the result of `renderScrollComponent` might change. It will be compared in
160+ * `shouldComponentUpdate`.
159161 */
160162 shouldUpdateToken ?: string ,
161163} ;
@@ -168,6 +170,11 @@ type State = {
168170class WindowedListView extends React . Component {
169171 props : Props ;
170172 state : State ;
173+ /**
174+ * Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates,
175+ * e.g. from multiple layout updates in rapid succession.
176+ */
177+ _computeRowsToRenderBatcher : Batchinator ;
171178 _firstVisible : number = - 1 ;
172179 _lastVisible : number = - 1 ;
173180 _scrollOffsetY : number = 0 ;
@@ -178,8 +185,6 @@ class WindowedListView extends React.Component {
178185 _rowFramesDirty: boolean = false ;
179186 _hasCalledOnEndReached: boolean = false ;
180187 _willComputeRowsToRender: boolean = false ;
181- _timeoutHandle: number = 0 ;
182- _incrementPending: boolean = false ;
183188 _viewableRows: Array < number > = [ ] ;
184189 _cellsInProgress: Set < string > = new Set ( ) ;
185190 _scrollRef: ?Object ;
@@ -191,7 +196,7 @@ class WindowedListView extends React.Component {
191196 viewablePercentThreshold : 50 ,
192197 renderScrollComponent : ( props ) => < ScrollView { ...props } /> ,
193198 disableIncrementalRendering : false ,
194- recomputeRowsBatchingPeriod : 10 , // This should capture most events that will happen in one frame
199+ recomputeRowsBatchingPeriod : 10 , // This should capture most events that happen within a frame
195200 } ;
196201
197202 constructor ( props : Props ) {
@@ -200,6 +205,10 @@ class WindowedListView extends React.Component {
200205 this . props . numToRenderAhead < this . props . maxNumToRender ,
201206 'WindowedListView: numToRenderAhead must be less than maxNumToRender'
202207 ) ;
208+ this . _computeRowsToRenderBatcher = new Batchinator (
209+ ( ) => this . _computeRowsToRender ( this . props ) ,
210+ this . props . recomputeRowsBatchingPeriod ,
211+ ) ;
203212 this . state = {
204213 firstRow : 0 ,
205214 lastRow : Math . min ( this . props . data . length , this . props . initialNumToRender ) - 1 ,
@@ -225,21 +234,27 @@ class WindowedListView extends React.Component {
225234 const newDataSubset = newProps . data . slice ( newState . firstRow , newState . lastRow + 1 ) ;
226235 const prevDataSubset = this . props . data . slice ( this . state . firstRow , this . state . lastRow + 1 ) ;
227236 if ( newDataSubset . length !== prevDataSubset . length ) {
228- DEBUG && infoLog ( ' yes, subset length: ' , { newLen : newDataSubset . length , oldLen : prevDataSubset . length } ) ;
237+ DEBUG && infoLog (
238+ ' yes, subset length: ' ,
239+ { newLen : newDataSubset . length , oldLen : prevDataSubset . length }
240+ ) ;
229241 return true ;
230242 }
231243 for ( let idx = 0 ; idx < newDataSubset . length ; idx ++ ) {
232244 if ( newDataSubset [ idx ] . rowData !== prevDataSubset [ idx ] . rowData ||
233245 newDataSubset [ idx ] . rowKey !== prevDataSubset [ idx ] . rowKey ) {
234- DEBUG && infoLog ( ' yes, data change: ' , { idx, new : newDataSubset [ idx ] , old : prevDataSubset [ idx ] } ) ;
246+ DEBUG && infoLog (
247+ ' yes, data change: ' ,
248+ { idx, new : newDataSubset [ idx ] , old : prevDataSubset [ idx ] }
249+ ) ;
235250 return true ;
236251 }
237252 }
238253 DEBUG && infoLog ( ' knope' ) ;
239254 return false ;
240255 }
241256 componentWillReceiveProps ( ) {
242- this . _enqueueComputeRowsToRender ( ) ;
257+ this . _computeRowsToRenderBatcher . schedule ( ) ;
243258 }
244259 _onMomentumScrollEnd = ( e : Object ) => {
245260 this . _onScroll ( e ) ;
@@ -252,7 +267,7 @@ class WindowedListView extends React.Component {
252267 // We don't want to enqueue any updates if any cells are in the middle of an incremental render,
253268 // because it would just be wasted work.
254269 if ( this . _cellsInProgress . size === 0 ) {
255- this . _enqueueComputeRowsToRender ( ) ;
270+ this . _computeRowsToRenderBatcher . schedule ( ) ;
256271 }
257272 if ( this . props . onViewableRowsChanged && Object . keys ( this . _rowFrames ) . length ) {
258273 const viewableRows = ViewabilityHelper . computeViewableRows (
@@ -273,24 +288,27 @@ class WindowedListView extends React.Component {
273288 _onNewLayout = ( params : { rowKey : string , layout : Object } ) => {
274289 const { rowKey, layout} = params ;
275290 if ( DEBUG ) {
276- const layoutPrev = this . _rowFrames [ rowKey ] || { } ;
291+ const prev = this . _rowFrames [ rowKey ] || { } ;
277292 infoLog (
278293 'record layout for row: ' ,
279- { k : rowKey , h : layout . height , y : layout . y , x : layout . x , hp : layoutPrev . height , yp : layoutPrev . y }
294+ { k : rowKey , h : layout . height , y : layout . y , x : layout . x , hp : prev . height , yp : prev . y }
280295 ) ;
281296 if ( this . _rowFrames [ rowKey ] ) {
282297 const deltaY = Math . abs ( this . _rowFrames [ rowKey ] . y - layout . y ) ;
283298 const deltaH = Math . abs ( this . _rowFrames [ rowKey ] . height - layout . height ) ;
284299 if ( deltaY > 2 || deltaH > 2 ) {
285300 const dataEntry = this . props . data . find ( ( datum ) => datum . rowKey === rowKey ) ;
286- console . warn ( 'layout jump: ' , { dataEntry, prevLayout : this . _rowFrames [ rowKey ] , newLayout : layout } ) ;
301+ console . warn (
302+ 'layout jump: ' ,
303+ { dataEntry, prevLayout : this . _rowFrames [ rowKey ] , newLayout : layout }
304+ ) ;
287305 }
288306 }
289307 }
290308 this . _rowFrames [ rowKey ] = { ...layout , offscreenLayoutDone : true } ;
291309 this . _rowFramesDirty = true ;
292310 if ( this . _cellsInProgress . size === 0 ) {
293- this . _enqueueComputeRowsToRender ( ) ;
311+ this . _computeRowsToRenderBatcher . schedule ( ) ;
294312 }
295313 } ;
296314 _onWillUnmountCell = ( rowKey : string ) => {
@@ -300,8 +318,8 @@ class WindowedListView extends React.Component {
300318 }
301319 } ;
302320 /**
303- * This is used to keep track of cells that are in the process of rendering. If any cells are in progress, then
304- * other updates are skipped because they will just be wasted work.
321+ * This is used to keep track of cells that are in the process of rendering. If any cells are in
322+ * progress, then other updates are skipped because they will just be wasted work.
305323 */
306324 _onProgressChange = ( { rowKey, inProgress} : { rowKey : string , inProgress : boolean } ) => {
307325 if ( inProgress ) {
@@ -310,26 +328,8 @@ class WindowedListView extends React.Component {
310328 this . _cellsInProgress . delete ( rowKey ) ;
311329 }
312330 } ;
313- /**
314- * Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates, e.g. from multiple
315- * layout updates in rapid succession.
316- */
317- _enqueueComputeRowsToRender ( ) : void {
318- if ( ! this . _willComputeRowsToRender ) {
319- this . _willComputeRowsToRender = true ; // batch up computations
320- clearTimeout ( this . _timeoutHandle ) ;
321- this . _timeoutHandle = setTimeout (
322- ( ) => {
323- this . _willComputeRowsToRender = false ;
324- this . _incrementPending = false ;
325- this . _computeRowsToRender ( this . props ) ;
326- } ,
327- this . props . recomputeRowsBatchingPeriod
328- ) ;
329- }
330- }
331331 componentWillUnmount ( ) {
332- clearTimeout ( this . _timeoutHandle ) ;
332+ this . _computeRowsToRenderBatcher . dispose ( ) ;
333333 }
334334 _computeRowsToRender ( props : Object ) : void {
335335 const totalRows = props . data . length ;
@@ -367,8 +367,9 @@ class WindowedListView extends React.Component {
367367 }
368368 this . _updateVisibleRows ( firstVisible , lastVisible ) ;
369369
370- // Unfortuantely, we can't use <Incremental> to simplify our increment logic in this function because we need to
371- // make sure that cells are rendered in the right order one at a time when scrolling back up.
370+ // Unfortuantely, we can't use <Incremental> to simplify our increment logic in this function
371+ // because we need to make sure that cells are rendered in the right order one at a time when
372+ // scrolling back up.
372373
373374 const numRendered = lastRow - this . state . firstRow + 1 ;
374375 // Our last row target that we will approach incrementally
@@ -378,14 +379,10 @@ class WindowedListView extends React.Component {
378379 totalRows - 1 , // Don't render past the end
379380 ) ;
380381 // Increment the last row one at a time per JS event loop
381- if ( ! this . _incrementPending ) {
382- if ( targetLastRow > this . state . lastRow ) {
383- lastRow ++ ;
384- this . _incrementPending = true ;
385- } else if ( targetLastRow < this . state . lastRow ) {
386- lastRow -- ;
387- this . _incrementPending = true ;
388- }
382+ if ( targetLastRow > this . state . lastRow ) {
383+ lastRow ++ ;
384+ } else if ( targetLastRow < this . state . lastRow ) {
385+ lastRow -- ;
389386 }
390387 // Once last row is set, figure out the first row
391388 const firstRow = Math . max (
@@ -415,7 +412,8 @@ class WindowedListView extends React.Component {
415412 const rowsShouldChange = firstRow !== this . state . firstRow || lastRow !== this . state . lastRow ;
416413 if ( this . _rowFramesDirty || rowsShouldChange ) {
417414 if ( rowsShouldChange ) {
418- props . onMountedRowsWillChange && props . onMountedRowsWillChange ( firstRow , lastRow - firstRow + 1 ) ;
415+ props . onMountedRowsWillChange &&
416+ props . onMountedRowsWillChange ( firstRow , lastRow - firstRow + 1 ) ;
419417 infoLog (
420418 'WLV: row render range will change:' ,
421419 { firstRow, firstVis : this . _firstVisible , lastVis : this . _lastVisible , lastRow} ,
@@ -441,21 +439,27 @@ class WindowedListView extends React.Component {
441439 const rowFrames = this . _rowFrames ;
442440 const rows = [ ] ;
443441 let spacerHeight = 0 ;
444- // Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50%
445- // of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say
446- // below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable
447- // incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also
448- // ignore the "buffer" size when we are bumped up against the edge of the available data.
442+ // Incremental rendering is a tradeoff between throughput and responsiveness. When we have
443+ // plenty of buffer (say 50% of the target), we render incrementally to keep the app responsive.
444+ // If we are dangerously low on buffer (say below 25%) we always disable incremental to try to
445+ // catch up as fast as possible. In the middle, we only disable incremental while scrolling
446+ // since it's unlikely the user will try to press a button while scrolling. We also ignore the
447+ // "buffer" size when we are bumped up against the edge of the available data.
449448 const firstBuffer = firstRow === 0 ? Infinity : this . _firstVisible - firstRow ;
450- const lastBuffer = lastRow === this . props . data . length - 1 ? Infinity : lastRow - this . _lastVisible ;
449+ const lastBuffer = lastRow === this . props . data . length - 1
450+ ? Infinity
451+ : lastRow - this . _lastVisible ;
451452 const minBuffer = Math . min ( firstBuffer , lastBuffer ) ;
452453 const disableIncrementalRendering = this . props . disableIncrementalRendering ||
453454 ( this . _isScrolling && minBuffer < this . props . numToRenderAhead * 0.5 ) ||
454455 ( minBuffer < this . props . numToRenderAhead * 0.25 ) ;
455456 // Render mode is sticky while the component is mounted.
456457 for ( let ii = firstRow ; ii <= lastRow ; ii ++ ) {
457458 const rowKey = this . props . data [ ii ] . rowKey ;
458- if ( this . _rowRenderMode [ rowKey ] === 'sync' || ( disableIncrementalRendering && this . _rowRenderMode [ rowKey ] !== 'async' ) ) {
459+ if (
460+ this . _rowRenderMode [ rowKey ] === 'sync' ||
461+ ( disableIncrementalRendering && this . _rowRenderMode [ rowKey ] !== 'async' )
462+ ) {
459463 this . _rowRenderMode [ rowKey ] = 'sync' ;
460464 } else {
461465 this . _rowRenderMode [ rowKey ] = 'async' ;
@@ -466,9 +470,9 @@ class WindowedListView extends React.Component {
466470 if ( ! rowFrames [ rowKey ] ) {
467471 break ; // if rowFrame missing, no following ones will exist so quit early
468472 }
469- // Look for the first row where offscreen layout is done (only true for mounted rows) or it will be rendered
470- // synchronously and set the spacer height such that it will offset all the unmounted rows before that one using
471- // the saved frame data.
473+ // Look for the first row where offscreen layout is done (only true for mounted rows) or it
474+ // will be rendered synchronously and set the spacer height such that it will offset all the
475+ // unmounted rows before that one using the saved frame data.
472476 if ( rowFrames [ rowKey ] . offscreenLayoutDone || this . _rowRenderMode [ rowKey ] === 'sync' ) {
473477 if ( ii > 0 ) {
474478 const prevRowKey = this . props . data [ ii - 1 ] . rowKey ;
@@ -479,15 +483,19 @@ class WindowedListView extends React.Component {
479483 }
480484 }
481485 let showIndicator = false ;
482- if ( spacerHeight > ( this . state . boundaryIndicatorHeight || 0 ) && this . props . renderWindowBoundaryIndicator ) {
486+ if (
487+ spacerHeight > ( this . state . boundaryIndicatorHeight || 0 ) &&
488+ this . props . renderWindowBoundaryIndicator
489+ ) {
483490 showIndicator = true ;
484491 spacerHeight -= this . state . boundaryIndicatorHeight || 0 ;
485492 }
486493 DEBUG && infoLog ( 'render top spacer with height ' , spacerHeight ) ;
487494 rows . push ( < View key = "sp-top" style = { { height : spacerHeight } } /> ) ;
488495 if ( this . props . renderWindowBoundaryIndicator ) {
489- // Always render it, even if removed, so that we can get the height right away and don't waste time creating/
490- // destroying it. Should see if there is a better spinner option that is not as expensive.
496+ // Always render it, even if removed, so that we can get the height right away and don't waste
497+ // time creating/ destroying it. Should see if there is a better spinner option that is not as
498+ // expensive.
491499 rows . push (
492500 < View
493501 style = { ! showIndicator && styles . remove }
@@ -590,26 +598,28 @@ type CellProps = {
590598 */
591599 asyncRowPerfEventName : ?string ,
592600 /**
593- * Initially false to indicate the cell should be rendered "offscreen" with position: absolute so that incremental
594- * rendering doesn't cause things to jump around. Once onNewLayout is called after offscreen rendering has completed,
595- * includeInLayout will be set true and the finished cell can be dropped into place.
601+ * Initially false to indicate the cell should be rendered "offscreen" with position: absolute so
602+ * that incremental rendering doesn't cause things to jump around. Once onNewLayout is called
603+ * after offscreen rendering has completed, includeInLayout will be set true and the finished cell
604+ * can be dropped into place.
596605 *
597- * This is coordinated outside this component so the parent can syncronize this re-render with managing the
598- * placeholder sizing.
606+ * This is coordinated outside this component so the parent can syncronize this re-render with
607+ * managing the placeholder sizing.
599608 */
600609 includeInLayout : boolean ,
601610 /**
602- * Updates the parent with the latest layout. Only called when incremental rendering is done and triggers the parent
603- * to re-render this row with includeInLayout true.
611+ * Updates the parent with the latest layout. Only called when incremental rendering is done and
612+ * triggers the parent to re-render this row with includeInLayout true.
604613 */
605614 onNewLayout : ( params : { rowKey : string , layout : Object } ) => void ,
606615 /**
607- * Used to track when rendering is in progress so the parent can avoid wastedful re-renders that are just going to be
608- * invalidated once the cell finishes.
616+ * Used to track when rendering is in progress so the parent can avoid wastedful re-renders that
617+ * are just going to be invalidated once the cell finishes.
609618 */
610619 onProgressChange : ( progress : { rowKey : string , inProgress : boolean } ) => void ,
611620 /**
612- * Used to invalidate the layout so the parent knows it needs to compensate for the height in the placeholder size.
621+ * Used to invalidate the layout so the parent knows it needs to compensate for the height in the
622+ * placeholder size.
613623 */
614624 onWillUnmount : ( rowKey : string ) => void ,
615625} ;
@@ -625,9 +635,12 @@ class CellRenderer extends React.Component {
625635 componentWillMount ( ) {
626636 if ( this . props . asyncRowPerfEventName ) {
627637 this . _perfUpdateID = g_perf_update_id ++ ;
628- this . _asyncCookie = Systrace . beginAsyncEvent ( this . props . asyncRowPerfEventName + this . _perfUpdateID ) ;
638+ this . _asyncCookie = Systrace . beginAsyncEvent (
639+ this . props . asyncRowPerfEventName + this . _perfUpdateID
640+ ) ;
629641 // $FlowFixMe(>=0.28.0)
630- infoLog ( `perf_asynctest_${ this . props . asyncRowPerfEventName } _start ${ this . _perfUpdateID } ${ Date . now ( ) } ` ) ;
642+ infoLog ( `perf_asynctest_${ this . props . asyncRowPerfEventName } _start ${ this . _perfUpdateID } ` +
643+ `${ Date . now ( ) } ` ) ;
631644 }
632645 if ( this . props . includeInLayout ) {
633646 this . _includeInLayoutLatch = true ;
@@ -650,21 +663,27 @@ class CellRenderer extends React.Component {
650663 invariant ( ! this . _offscreenRenderDone , 'should only finish rendering once' ) ;
651664 this . _offscreenRenderDone = true ;
652665
653- // If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero,
654- // and thus the onNewLayout call will not fire the needed state change update.
666+ // If this is not called before calling onNewLayout, the number of inProgress cells will remain
667+ // non-zero, and thus the onNewLayout call will not fire the needed state change update.
655668 this . props . onProgressChange ( { rowKey : this . props . rowKey , inProgress : false } ) ;
656669
657- // If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens
658- // when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen.
659- this . _lastLayout && this . props . onNewLayout ( { rowKey : this . props . rowKey , layout : this . _lastLayout } ) ;
670+ // If an onLayout event hasn't come in yet, then we skip here and assume it will come in later.
671+ // This happens when Incremental is disabled and _onOffscreenRenderDone is called faster than
672+ // layout can happen.
673+ this . _lastLayout &&
674+ this . props . onNewLayout ( { rowKey : this . props . rowKey , layout : this . _lastLayout } ) ;
660675
661676 DEBUG && infoLog ( '\n >>>>> display row ' + this . props . rowIndex + '\n\n\n' ) ;
662677 if ( this . props . asyncRowPerfEventName ) {
663- // Note this doesn't include the native render time but is more accurate than also including the JS render
664- // time of anything that has been queued up.
665- Systrace . endAsyncEvent ( this . props . asyncRowPerfEventName + this . _perfUpdateID , this . _asyncCookie ) ;
678+ // Note this doesn't include the native render time but is more accurate than also including
679+ // the JS render time of anything that has been queued up.
680+ Systrace . endAsyncEvent (
681+ this . props . asyncRowPerfEventName + this . _perfUpdateID ,
682+ this . _asyncCookie
683+ ) ;
666684 // $FlowFixMe(>=0.28.0)
667- infoLog ( `perf_asynctest_${ this . props . asyncRowPerfEventName } _end ${ this . _perfUpdateID } ${ Date . now ( ) } ` ) ;
685+ infoLog ( `perf_asynctest_${ this . props . asyncRowPerfEventName } _end ${ this . _perfUpdateID } ` +
686+ `${ Date . now ( ) } ` ) ;
668687 }
669688 }
670689 _onOffscreenRenderDone = ( ) => {
0 commit comments