mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-22 11:16:06 +08:00
Use Batchinator in WindowedListView
Summary: Cleans things up and also defers rendering rows if there is an interaction happening. Reviewed By: achen1 Differential Revision: D3817231 fbshipit-source-id: fd08d0ca7cb6c203178f27bfc5a0f55469135c3a
This commit is contained in:
committed by
Facebook Github Bot 1
parent
322c160fb1
commit
6fb149596a
@@ -32,6 +32,7 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Batchinator = require('Batchinator');
|
||||
const IncrementalGroup = require('IncrementalGroup');
|
||||
const React = require('React');
|
||||
const ScrollView = require('ScrollView');
|
||||
@@ -149,13 +150,14 @@ type Props = {
|
||||
*/
|
||||
recomputeRowsBatchingPeriod: number,
|
||||
/**
|
||||
* Called when rows will be mounted/unmounted. Mounted rows always form a contiguous block so it is expressed as a
|
||||
* range of start plus count.
|
||||
* Called when rows will be mounted/unmounted. Mounted rows always form a contiguous block so it
|
||||
* is expressed as a range of start plus count.
|
||||
*/
|
||||
onMountedRowsWillChange?: (firstIdx: number, count: number) => void,
|
||||
/**
|
||||
* Change this when you want to make sure the WindowedListView will re-render, for example when the result of
|
||||
* `renderScrollComponent` might change. It will be compared in `shouldComponentUpdate`.
|
||||
* Change this when you want to make sure the WindowedListView will re-render, for example when
|
||||
* the result of `renderScrollComponent` might change. It will be compared in
|
||||
* `shouldComponentUpdate`.
|
||||
*/
|
||||
shouldUpdateToken?: string,
|
||||
};
|
||||
@@ -168,6 +170,11 @@ type State = {
|
||||
class WindowedListView extends React.Component {
|
||||
props: Props;
|
||||
state: State;
|
||||
/**
|
||||
* Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates,
|
||||
* e.g. from multiple layout updates in rapid succession.
|
||||
*/
|
||||
_computeRowsToRenderBatcher: Batchinator;
|
||||
_firstVisible: number = -1;
|
||||
_lastVisible: number = -1;
|
||||
_scrollOffsetY: number = 0;
|
||||
@@ -178,8 +185,6 @@ class WindowedListView extends React.Component {
|
||||
_rowFramesDirty: boolean = false;
|
||||
_hasCalledOnEndReached: boolean = false;
|
||||
_willComputeRowsToRender: boolean = false;
|
||||
_timeoutHandle: number = 0;
|
||||
_incrementPending: boolean = false;
|
||||
_viewableRows: Array<number> = [];
|
||||
_cellsInProgress: Set<string> = new Set();
|
||||
_scrollRef: ?Object;
|
||||
@@ -191,7 +196,7 @@ class WindowedListView extends React.Component {
|
||||
viewablePercentThreshold: 50,
|
||||
renderScrollComponent: (props) => <ScrollView {...props} />,
|
||||
disableIncrementalRendering: false,
|
||||
recomputeRowsBatchingPeriod: 10, // This should capture most events that will happen in one frame
|
||||
recomputeRowsBatchingPeriod: 10, // This should capture most events that happen within a frame
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -200,6 +205,10 @@ class WindowedListView extends React.Component {
|
||||
this.props.numToRenderAhead < this.props.maxNumToRender,
|
||||
'WindowedListView: numToRenderAhead must be less than maxNumToRender'
|
||||
);
|
||||
this._computeRowsToRenderBatcher = new Batchinator(
|
||||
() => this._computeRowsToRender(this.props),
|
||||
this.props.recomputeRowsBatchingPeriod,
|
||||
);
|
||||
this.state = {
|
||||
firstRow: 0,
|
||||
lastRow: Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
|
||||
@@ -225,13 +234,19 @@ class WindowedListView extends React.Component {
|
||||
const newDataSubset = newProps.data.slice(newState.firstRow, newState.lastRow + 1);
|
||||
const prevDataSubset = this.props.data.slice(this.state.firstRow, this.state.lastRow + 1);
|
||||
if (newDataSubset.length !== prevDataSubset.length) {
|
||||
DEBUG && infoLog(' yes, subset length: ', {newLen: newDataSubset.length, oldLen: prevDataSubset.length});
|
||||
DEBUG && infoLog(
|
||||
' yes, subset length: ',
|
||||
{newLen: newDataSubset.length, oldLen: prevDataSubset.length}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
for (let idx = 0; idx < newDataSubset.length; idx++) {
|
||||
if (newDataSubset[idx].rowData !== prevDataSubset[idx].rowData ||
|
||||
newDataSubset[idx].rowKey !== prevDataSubset[idx].rowKey) {
|
||||
DEBUG && infoLog(' yes, data change: ', {idx, new: newDataSubset[idx], old: prevDataSubset[idx]});
|
||||
DEBUG && infoLog(
|
||||
' yes, data change: ',
|
||||
{idx, new: newDataSubset[idx], old: prevDataSubset[idx]}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +254,7 @@ class WindowedListView extends React.Component {
|
||||
return false;
|
||||
}
|
||||
componentWillReceiveProps() {
|
||||
this._enqueueComputeRowsToRender();
|
||||
this._computeRowsToRenderBatcher.schedule();
|
||||
}
|
||||
_onMomentumScrollEnd = (e: Object) => {
|
||||
this._onScroll(e);
|
||||
@@ -252,7 +267,7 @@ class WindowedListView extends React.Component {
|
||||
// We don't want to enqueue any updates if any cells are in the middle of an incremental render,
|
||||
// because it would just be wasted work.
|
||||
if (this._cellsInProgress.size === 0) {
|
||||
this._enqueueComputeRowsToRender();
|
||||
this._computeRowsToRenderBatcher.schedule();
|
||||
}
|
||||
if (this.props.onViewableRowsChanged && Object.keys(this._rowFrames).length) {
|
||||
const viewableRows = ViewabilityHelper.computeViewableRows(
|
||||
@@ -273,24 +288,27 @@ class WindowedListView extends React.Component {
|
||||
_onNewLayout = (params: {rowKey: string, layout: Object}) => {
|
||||
const {rowKey, layout} = params;
|
||||
if (DEBUG) {
|
||||
const layoutPrev = this._rowFrames[rowKey] || {};
|
||||
const prev = this._rowFrames[rowKey] || {};
|
||||
infoLog(
|
||||
'record layout for row: ',
|
||||
{k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y}
|
||||
{k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: prev.height, yp: prev.y}
|
||||
);
|
||||
if (this._rowFrames[rowKey]) {
|
||||
const deltaY = Math.abs(this._rowFrames[rowKey].y - layout.y);
|
||||
const deltaH = Math.abs(this._rowFrames[rowKey].height - layout.height);
|
||||
if (deltaY > 2 || deltaH > 2) {
|
||||
const dataEntry = this.props.data.find((datum) => datum.rowKey === rowKey);
|
||||
console.warn('layout jump: ', {dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout});
|
||||
console.warn(
|
||||
'layout jump: ',
|
||||
{dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rowFrames[rowKey] = {...layout, offscreenLayoutDone: true};
|
||||
this._rowFramesDirty = true;
|
||||
if (this._cellsInProgress.size === 0) {
|
||||
this._enqueueComputeRowsToRender();
|
||||
this._computeRowsToRenderBatcher.schedule();
|
||||
}
|
||||
};
|
||||
_onWillUnmountCell = (rowKey: string) => {
|
||||
@@ -300,8 +318,8 @@ class WindowedListView extends React.Component {
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This is used to keep track of cells that are in the process of rendering. If any cells are in progress, then
|
||||
* other updates are skipped because they will just be wasted work.
|
||||
* This is used to keep track of cells that are in the process of rendering. If any cells are in
|
||||
* progress, then other updates are skipped because they will just be wasted work.
|
||||
*/
|
||||
_onProgressChange = ({rowKey, inProgress}: {rowKey: string, inProgress: boolean}) => {
|
||||
if (inProgress) {
|
||||
@@ -310,26 +328,8 @@ class WindowedListView extends React.Component {
|
||||
this._cellsInProgress.delete(rowKey);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates, e.g. from multiple
|
||||
* layout updates in rapid succession.
|
||||
*/
|
||||
_enqueueComputeRowsToRender(): void {
|
||||
if (!this._willComputeRowsToRender) {
|
||||
this._willComputeRowsToRender = true; // batch up computations
|
||||
clearTimeout(this._timeoutHandle);
|
||||
this._timeoutHandle = setTimeout(
|
||||
() => {
|
||||
this._willComputeRowsToRender = false;
|
||||
this._incrementPending = false;
|
||||
this._computeRowsToRender(this.props);
|
||||
},
|
||||
this.props.recomputeRowsBatchingPeriod
|
||||
);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this._timeoutHandle);
|
||||
this._computeRowsToRenderBatcher.dispose();
|
||||
}
|
||||
_computeRowsToRender(props: Object): void {
|
||||
const totalRows = props.data.length;
|
||||
@@ -367,8 +367,9 @@ class WindowedListView extends React.Component {
|
||||
}
|
||||
this._updateVisibleRows(firstVisible, lastVisible);
|
||||
|
||||
// Unfortuantely, we can't use <Incremental> to simplify our increment logic in this function because we need to
|
||||
// make sure that cells are rendered in the right order one at a time when scrolling back up.
|
||||
// Unfortuantely, we can't use <Incremental> to simplify our increment logic in this function
|
||||
// because we need to make sure that cells are rendered in the right order one at a time when
|
||||
// scrolling back up.
|
||||
|
||||
const numRendered = lastRow - this.state.firstRow + 1;
|
||||
// Our last row target that we will approach incrementally
|
||||
@@ -378,14 +379,10 @@ class WindowedListView extends React.Component {
|
||||
totalRows - 1, // Don't render past the end
|
||||
);
|
||||
// Increment the last row one at a time per JS event loop
|
||||
if (!this._incrementPending) {
|
||||
if (targetLastRow > this.state.lastRow) {
|
||||
lastRow++;
|
||||
this._incrementPending = true;
|
||||
} else if (targetLastRow < this.state.lastRow) {
|
||||
lastRow--;
|
||||
this._incrementPending = true;
|
||||
}
|
||||
if (targetLastRow > this.state.lastRow) {
|
||||
lastRow++;
|
||||
} else if (targetLastRow < this.state.lastRow) {
|
||||
lastRow--;
|
||||
}
|
||||
// Once last row is set, figure out the first row
|
||||
const firstRow = Math.max(
|
||||
@@ -415,7 +412,8 @@ class WindowedListView extends React.Component {
|
||||
const rowsShouldChange = firstRow !== this.state.firstRow || lastRow !== this.state.lastRow;
|
||||
if (this._rowFramesDirty || rowsShouldChange) {
|
||||
if (rowsShouldChange) {
|
||||
props.onMountedRowsWillChange && props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1);
|
||||
props.onMountedRowsWillChange &&
|
||||
props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1);
|
||||
infoLog(
|
||||
'WLV: row render range will change:',
|
||||
{firstRow, firstVis: this._firstVisible, lastVis: this._lastVisible, lastRow},
|
||||
@@ -441,13 +439,16 @@ class WindowedListView extends React.Component {
|
||||
const rowFrames = this._rowFrames;
|
||||
const rows = [];
|
||||
let spacerHeight = 0;
|
||||
// Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50%
|
||||
// of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say
|
||||
// below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable
|
||||
// incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also
|
||||
// ignore the "buffer" size when we are bumped up against the edge of the available data.
|
||||
// Incremental rendering is a tradeoff between throughput and responsiveness. When we have
|
||||
// plenty of buffer (say 50% of the target), we render incrementally to keep the app responsive.
|
||||
// If we are dangerously low on buffer (say below 25%) we always disable incremental to try to
|
||||
// catch up as fast as possible. In the middle, we only disable incremental while scrolling
|
||||
// since it's unlikely the user will try to press a button while scrolling. We also ignore the
|
||||
// "buffer" size when we are bumped up against the edge of the available data.
|
||||
const firstBuffer = firstRow === 0 ? Infinity : this._firstVisible - firstRow;
|
||||
const lastBuffer = lastRow === this.props.data.length - 1 ? Infinity : lastRow - this._lastVisible;
|
||||
const lastBuffer = lastRow === this.props.data.length - 1
|
||||
? Infinity
|
||||
: lastRow - this._lastVisible;
|
||||
const minBuffer = Math.min(firstBuffer, lastBuffer);
|
||||
const disableIncrementalRendering = this.props.disableIncrementalRendering ||
|
||||
(this._isScrolling && minBuffer < this.props.numToRenderAhead * 0.5) ||
|
||||
@@ -455,7 +456,10 @@ class WindowedListView extends React.Component {
|
||||
// Render mode is sticky while the component is mounted.
|
||||
for (let ii = firstRow; ii <= lastRow; ii++) {
|
||||
const rowKey = this.props.data[ii].rowKey;
|
||||
if (this._rowRenderMode[rowKey] === 'sync' || (disableIncrementalRendering && this._rowRenderMode[rowKey] !== 'async')) {
|
||||
if (
|
||||
this._rowRenderMode[rowKey] === 'sync' ||
|
||||
(disableIncrementalRendering && this._rowRenderMode[rowKey] !== 'async')
|
||||
) {
|
||||
this._rowRenderMode[rowKey] = 'sync';
|
||||
} else {
|
||||
this._rowRenderMode[rowKey] = 'async';
|
||||
@@ -466,9 +470,9 @@ class WindowedListView extends React.Component {
|
||||
if (!rowFrames[rowKey]) {
|
||||
break; // if rowFrame missing, no following ones will exist so quit early
|
||||
}
|
||||
// Look for the first row where offscreen layout is done (only true for mounted rows) or it will be rendered
|
||||
// synchronously and set the spacer height such that it will offset all the unmounted rows before that one using
|
||||
// the saved frame data.
|
||||
// Look for the first row where offscreen layout is done (only true for mounted rows) or it
|
||||
// will be rendered synchronously and set the spacer height such that it will offset all the
|
||||
// unmounted rows before that one using the saved frame data.
|
||||
if (rowFrames[rowKey].offscreenLayoutDone || this._rowRenderMode[rowKey] === 'sync') {
|
||||
if (ii > 0) {
|
||||
const prevRowKey = this.props.data[ii - 1].rowKey;
|
||||
@@ -479,15 +483,19 @@ class WindowedListView extends React.Component {
|
||||
}
|
||||
}
|
||||
let showIndicator = false;
|
||||
if (spacerHeight > (this.state.boundaryIndicatorHeight || 0) && this.props.renderWindowBoundaryIndicator) {
|
||||
if (
|
||||
spacerHeight > (this.state.boundaryIndicatorHeight || 0) &&
|
||||
this.props.renderWindowBoundaryIndicator
|
||||
) {
|
||||
showIndicator = true;
|
||||
spacerHeight -= this.state.boundaryIndicatorHeight || 0;
|
||||
}
|
||||
DEBUG && infoLog('render top spacer with height ', spacerHeight);
|
||||
rows.push(<View key="sp-top" style={{height: spacerHeight}} />);
|
||||
if (this.props.renderWindowBoundaryIndicator) {
|
||||
// Always render it, even if removed, so that we can get the height right away and don't waste time creating/
|
||||
// destroying it. Should see if there is a better spinner option that is not as expensive.
|
||||
// Always render it, even if removed, so that we can get the height right away and don't waste
|
||||
// time creating/ destroying it. Should see if there is a better spinner option that is not as
|
||||
// expensive.
|
||||
rows.push(
|
||||
<View
|
||||
style={!showIndicator && styles.remove}
|
||||
@@ -590,26 +598,28 @@ type CellProps = {
|
||||
*/
|
||||
asyncRowPerfEventName: ?string,
|
||||
/**
|
||||
* Initially false to indicate the cell should be rendered "offscreen" with position: absolute so that incremental
|
||||
* rendering doesn't cause things to jump around. Once onNewLayout is called after offscreen rendering has completed,
|
||||
* includeInLayout will be set true and the finished cell can be dropped into place.
|
||||
* Initially false to indicate the cell should be rendered "offscreen" with position: absolute so
|
||||
* that incremental rendering doesn't cause things to jump around. Once onNewLayout is called
|
||||
* after offscreen rendering has completed, includeInLayout will be set true and the finished cell
|
||||
* can be dropped into place.
|
||||
*
|
||||
* This is coordinated outside this component so the parent can syncronize this re-render with managing the
|
||||
* placeholder sizing.
|
||||
* This is coordinated outside this component so the parent can syncronize this re-render with
|
||||
* managing the placeholder sizing.
|
||||
*/
|
||||
includeInLayout: boolean,
|
||||
/**
|
||||
* Updates the parent with the latest layout. Only called when incremental rendering is done and triggers the parent
|
||||
* to re-render this row with includeInLayout true.
|
||||
* Updates the parent with the latest layout. Only called when incremental rendering is done and
|
||||
* triggers the parent to re-render this row with includeInLayout true.
|
||||
*/
|
||||
onNewLayout: (params: {rowKey: string, layout: Object}) => void,
|
||||
/**
|
||||
* Used to track when rendering is in progress so the parent can avoid wastedful re-renders that are just going to be
|
||||
* invalidated once the cell finishes.
|
||||
* Used to track when rendering is in progress so the parent can avoid wastedful re-renders that
|
||||
* are just going to be invalidated once the cell finishes.
|
||||
*/
|
||||
onProgressChange: (progress: {rowKey: string, inProgress: boolean}) => void,
|
||||
/**
|
||||
* Used to invalidate the layout so the parent knows it needs to compensate for the height in the placeholder size.
|
||||
* Used to invalidate the layout so the parent knows it needs to compensate for the height in the
|
||||
* placeholder size.
|
||||
*/
|
||||
onWillUnmount: (rowKey: string) => void,
|
||||
};
|
||||
@@ -625,9 +635,12 @@ class CellRenderer extends React.Component {
|
||||
componentWillMount() {
|
||||
if (this.props.asyncRowPerfEventName) {
|
||||
this._perfUpdateID = g_perf_update_id++;
|
||||
this._asyncCookie = Systrace.beginAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID);
|
||||
this._asyncCookie = Systrace.beginAsyncEvent(
|
||||
this.props.asyncRowPerfEventName + this._perfUpdateID
|
||||
);
|
||||
// $FlowFixMe(>=0.28.0)
|
||||
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`);
|
||||
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ` +
|
||||
`${Date.now()}`);
|
||||
}
|
||||
if (this.props.includeInLayout) {
|
||||
this._includeInLayoutLatch = true;
|
||||
@@ -650,21 +663,27 @@ class CellRenderer extends React.Component {
|
||||
invariant(!this._offscreenRenderDone, 'should only finish rendering once');
|
||||
this._offscreenRenderDone = true;
|
||||
|
||||
// If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero,
|
||||
// and thus the onNewLayout call will not fire the needed state change update.
|
||||
// If this is not called before calling onNewLayout, the number of inProgress cells will remain
|
||||
// non-zero, and thus the onNewLayout call will not fire the needed state change update.
|
||||
this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false});
|
||||
|
||||
// If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens
|
||||
// when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen.
|
||||
this._lastLayout && this.props.onNewLayout({rowKey: this.props.rowKey, layout: this._lastLayout});
|
||||
// If an onLayout event hasn't come in yet, then we skip here and assume it will come in later.
|
||||
// This happens when Incremental is disabled and _onOffscreenRenderDone is called faster than
|
||||
// layout can happen.
|
||||
this._lastLayout &&
|
||||
this.props.onNewLayout({rowKey: this.props.rowKey, layout: this._lastLayout});
|
||||
|
||||
DEBUG && infoLog('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n');
|
||||
if (this.props.asyncRowPerfEventName) {
|
||||
// Note this doesn't include the native render time but is more accurate than also including the JS render
|
||||
// time of anything that has been queued up.
|
||||
Systrace.endAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID, this._asyncCookie);
|
||||
// Note this doesn't include the native render time but is more accurate than also including
|
||||
// the JS render time of anything that has been queued up.
|
||||
Systrace.endAsyncEvent(
|
||||
this.props.asyncRowPerfEventName + this._perfUpdateID,
|
||||
this._asyncCookie
|
||||
);
|
||||
// $FlowFixMe(>=0.28.0)
|
||||
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ${Date.now()}`);
|
||||
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ` +
|
||||
`${Date.now()}`);
|
||||
}
|
||||
}
|
||||
_onOffscreenRenderDone = () => {
|
||||
|
||||
Reference in New Issue
Block a user