Created
October 1, 2015 04:16
-
-
Save sahrens/d1802e7a96b4c261f0b5 to your computer and use it in GitHub Desktop.
MultiRowListView is an experimental ListView with two potential optimizations - incremental row rendering via subrows, and React row recycling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2004-present Facebook. All Rights Reserved. | |
* | |
* @providesModule MultiRowListView | |
* @flow | |
*/ | |
'use strict'; | |
var React = require('React'); | |
var ScrollView = require('ScrollView'); | |
var Text = require('Text'); | |
var View = require('View'); | |
var DEBUG = false; | |
class CellRenderer extends React.Component { | |
shouldComponentUpdate(newProps) { | |
return newProps.shouldUpdate; | |
} | |
render() { | |
var debug = DEBUG && <Text style={{backgroundColor: 'lightblue'}}> | |
React Key: {this.props.reactKey}, | |
Row: {this.props.rowIndex}, | |
Id: {this.props.data.__dataID__} | |
</Text>; | |
// var start = Date.now(); | |
// while (Date.now() < start + 50) {} // burn cpu to test perf effect. | |
return ( | |
<View | |
ref={(ref) => this.viewRef = ref} | |
onLayout={(e) => { | |
this.props.onMeasure(e.nativeEvent.layout); | |
}}> | |
{debug} | |
{this.props.render()} | |
</View> | |
); | |
} | |
} | |
CellRenderer.propTypes = { | |
shouldUpdate: React.PropTypes.bool, | |
render: React.PropTypes.func, | |
}; | |
class MultiRowListView extends React.Component { | |
_rowCache: Array<number>; | |
constructor(props: Object) { | |
super(props); | |
this.onScroll = this.onScroll.bind(this); | |
this.computeRowReindexes = this.computeRowReindexes.bind(this); | |
this.computeRowsToRender = this.computeRowsToRender.bind(this); | |
this.computeRowsToRenderSync = this.computeRowsToRenderSync.bind(this); | |
this.scrollOffsetY = 0; | |
this.rowFrames = []; | |
this.calledOnEndReached = false; | |
this.computeRowReindexes(props); | |
this.workingSetSubrowCount = props.workingSetSubrowCount; | |
this.state = { | |
firstRow: 0, | |
lastRow: Math.min(this.workingSetSubrowCount, this.rowReindexes.length) - 1, | |
}; | |
} | |
getScrollResponder(): ?ReactComponent { | |
return this.scrollRef && | |
this.scrollRef.getScrollResponder && | |
this.scrollRef.getScrollResponder(); | |
} | |
onScroll(e: Object) { | |
var scrollOffsetY = e.nativeEvent.contentOffset.y; | |
if (Math.abs(this.scrollOffsetY - scrollOffsetY) > 50) { | |
this.scrollOffsetY = scrollOffsetY; | |
this.computeRowsToRender(); | |
} | |
} | |
componentWillReceiveProps(newProps: Object) { | |
this.computeRowReindexes(newProps); | |
this.computeRowsToRender(); | |
} | |
computeRowReindexes(props: Object) { | |
var dataIdx = 0; | |
var subrowIdx = 0; | |
this.rowReindexes = []; | |
for (var dataIdx = 0; dataIdx < props.data.length; dataIdx++) { | |
var numSubrows = props.getSubrowCountForRow(props.data[dataIdx]); | |
for (var subrowIdx = 0; subrowIdx < numSubrows; subrowIdx++) { | |
this.rowReindexes.push({dataIdx, subrowIdx}); | |
} | |
} | |
DEBUG && console.log('computeRowReindexes, length: ' + this.rowReindexes.length); | |
} | |
_onMeasure(idx: number, layout: Object) { | |
if (!this.rowFrames[idx] || | |
layout.height !== this.rowFrames[idx].height || | |
layout.y !== this.rowFrames[idx].y) { | |
this.rowFrames[idx] = {...layout}; | |
this.computeRowsToRender(); | |
} | |
} | |
computeRowsToRender(): void { | |
if (!this.willComputeRowsToRender) { | |
this.willComputeRowsToRender = true; // batch up computations | |
setImmediate(this.computeRowsToRenderSync); | |
} | |
} | |
computeRowsToRenderSync(): void { | |
this.willComputeRowsToRender = false; | |
var rowFrames = this.rowFrames; | |
var totalRows = this.rowReindexes.length; | |
var firstVisible; | |
var top = this.scrollOffsetY; | |
for (var idx = 0; idx < rowFrames.length; idx++) { | |
var frame = rowFrames[idx]; | |
if (!frame) { | |
console.warn('waa? No frame :( Should come soon.'); | |
return; | |
} | |
if ((frame.y + frame.height) > top) { | |
firstVisible = idx; | |
break; | |
} | |
} | |
if (firstVisible === undefined) { | |
firstVisible = rowFrames.length; | |
console.warn('overrun', {firstVisible, num2render: this.workingSetSubrowCount}); | |
} | |
var firstRow = Math.max((firstVisible || 0) - this.props.topBufferSubrowCount, 0); | |
var lastRow = Math.min(firstRow + this.workingSetSubrowCount - 1, totalRows - 1); | |
if (lastRow - firstRow < this.workingSetSubrowCount - 1) { | |
// If we're close to the bottom, we still want to render numToRender rows | |
firstRow = Math.max(0, lastRow - this.workingSetSubrowCount + 1); | |
} | |
if (!this.props.recycleSubrows) { | |
firstRow = 0; | |
} | |
var rowCountIncrease = (lastRow - firstRow) - (this.state.lastRow - this.state.firstRow); | |
if (rowCountIncrease > this.props.pageSize) { | |
lastRow += (this.props.pageSize - rowCountIncrease); | |
} else if (rowCountIncrease < 0) { | |
console.warn('negative increase!', {firstRow, lastRow, totalRows, rowCountIncrease}); | |
// lastRow = totalRows - 1; // Sometimes the data will shrink on us. | |
if (firstRow > lastRow) { | |
console.warn('first row got ahead of last row!'); | |
firstRow = lastRow; | |
} | |
} | |
if (this.props.onEndReached) { | |
// This logic is a little tricky to make sure we call onEndReached exactly | |
// once every time we reach the end. | |
var willBeAtTheEnd = lastRow === (totalRows - 1); | |
if (willBeAtTheEnd && !this.calledOnEndReached) { | |
this.props.onEndReached(); | |
this.calledOnEndReached = true; | |
} else { | |
// If the last row is changing, then we haven't called onEndReached for | |
// the new end. | |
this.calledOnEndReached = this.state.lastRow === lastRow; | |
} | |
} | |
if (this.state.firstRow !== firstRow || this.state.lastRow !== lastRow) { | |
var count = lastRow - firstRow + 1; | |
if (count !== this.workingSetSubrowCount && firstRow !== 0 && this.workingSetSubrowCount < totalRows) { | |
console.warn('Should always render same number of rows for optimal ' + | |
'perf. Rendered ' + count + ' with target ' + | |
this.workingSetSubrowCount); | |
} | |
DEBUG && (rowCountIncrease = ((lastRow - firstRow) - (this.state.lastRow - this.state.firstRow))); | |
DEBUG && console.log('setState new row indexes', {rowCountIncrease, firstRow, lastRow, count, firstVisible, top}); | |
this.setState({firstRow, lastRow}); | |
} | |
} | |
render() { | |
this._rowCache = this._rowCache || []; | |
var firstRow = this.state.firstRow; | |
var lastRow = this.state.lastRow; | |
var rowFrames = this.rowFrames; | |
var rows = []; | |
var height = 0; | |
var avgHeight = 100; | |
if (rowFrames[firstRow]) { | |
var firstFrame = rowFrames[firstRow]; | |
height = firstFrame.y; | |
avgHeight = height / (firstRow + 1); // TODO: use the last row with a frame | |
// console.log('compute avgHeight1', {avgHeight, height, firstRow}); | |
} | |
var rowsMeta = []; | |
var topHeight = height; | |
rows.push(<View key={'sp-top'} style={{height}} />); | |
var fullRowCount = lastRow - firstRow + 1; | |
for (var idx = firstRow; idx <= lastRow; idx++) { | |
height += rowFrames[idx] ? rowFrames[idx].height : avgHeight; | |
if (!this.rowReindexes[idx]) { | |
console.warn( | |
'Data missing...', | |
{idx, rowReindexes: this.rowReindexes, firstRow, lastRow} | |
); | |
break; | |
} | |
var data = this.props.data[this.rowReindexes[idx].dataIdx]; | |
var key = idx % fullRowCount; | |
var shouldUpdate = data !== this._rowCache[key]; | |
rows.push( | |
<CellRenderer | |
key={key} | |
reactKey={key} | |
rowIndex={idx} | |
data={data} | |
onMeasure={this._onMeasure.bind(this, idx)} | |
shouldUpdate={shouldUpdate} | |
render={this.props.renderSubrow.bind( | |
null, data, this.rowReindexes[idx].subrowIdx | |
)} | |
/> | |
); | |
this._rowCache[key] = data; | |
rowsMeta.push({key, idx, savedFrame: rowFrames[idx], shouldUpdate}); | |
} | |
if (this.props.renderFooter) { | |
rows.push(this.props.renderFooter()); | |
} | |
if (lastRow && height) { | |
avgHeight = height / lastRow; | |
} | |
height = (this.rowReindexes.length - lastRow - 1) * avgHeight; // This allows overrun for huge lists - maybe we don't want this? | |
rows.push(<View key={'sp-bottom'} style={{height}} />); | |
DEBUG && console.log('rec list render', {/*rowsMeta,*/ avgHeight, topHeight, botHeight: height, /*rowFrames,*/ firstRow, lastRow, countAll: rows.length}); | |
return ( | |
<ScrollView | |
{...this.props} | |
ref={(ref) => { this.scrollRef = ref; }} | |
onScroll={this.onScroll} | |
scrollEventThrottle={50}> | |
{rows} | |
</ScrollView> | |
); | |
} | |
} | |
MultiRowListView.propTypes = { | |
/** | |
* Simple array of data blobs, one per row. renderSubrow will be called with | |
* the same data blob for all the subrows that belong to the same row. | |
*/ | |
data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, | |
/** | |
* `getSubrowCountForRow(rowData: any): number` | |
*/ | |
getSubrowCountForRow: React.PropTypes.func.isRequired, | |
/** | |
* `renderSubrow(rowData: any, subrowIndex: number): ReactElement` | |
*/ | |
renderSubrow: React.PropTypes.func.isRequired, | |
/** | |
* How many subrows to render above the viewport. | |
*/ | |
topBufferSubrowCount: React.PropTypes.number, | |
/** | |
* WARNING: EXPERIMENTAL | |
* | |
* Whether react row components should be recycled. Currently breaks any usage | |
* of setState or instance variables anywhere within the row components or | |
* their children. | |
* | |
* It's recommended that you always return the same number of subrows for | |
* every row (`getSubrowCountForRow` should return a constant) and provide | |
* zero-height views if you don't have anything to render for a particular | |
* subrow index to make recycling more efficient. | |
*/ | |
recycleSubrows: React.PropTypes.bool, | |
}; | |
MultiRowListView.defaultProps = { | |
pageSize: 1, | |
workingSetSubrowCount: 20, | |
recycleSubrows: false, | |
topBufferSubrowCount: 4, | |
}; | |
module.exports = MultiRowListView; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment