ReactNative列表是基于ScrollView的,并没有直接使用IOS或Android的原生列表组件。因为RN真正调用native代码是异步的,并不能保证同步,而在native环境中,所有即将在视窗中呈现的元素都必须同步渲染,超过一定的时间(ios为16ms)就会出现掉帧,所以RN采用ScrollView作为列表组件的基础。
ReactNative刚开始提供的是ListView组件,在数据量大的情况下,性能特别差。目前提供的列表组件是FlatList和SectionList,性能问题得到了很好的解决,它们都是基于VirtualizedList。
下文引用的源码基于ReactNative 0.51,node_modules/react-native/Libraries/Lists/目录下。
VirtualizedList原理
- 每次新增绘制item的最大数量为10,循环绘制(以10为单位累加绘制);
- 首先绘制显示在屏幕中的items,再根据优先级循环绘制屏幕上显示items相近的数据,直至绘制完成;
- 每次绘制过程中,所有不需要绘制的元素用空View代替;
源码分析
render
方法(空间原因有删减):
const {
ListEmptyComponent,//数据为空显示样式
ListFooterComponent,//footer
ListHeaderComponent,//header
} = this.props;
const {data, horizontal} = this.props;
const isVirtualizationDisabled = this._isVirtualizationDisabled();
//确定反转样式
const inversionStyle = this.props.inverted
? this.props.horizontal ? styles.horizontallyInverted : styles.verticallyInverted
: null;
const cells = [];//list显示view集合
//section集合(即SectionList中的分组)
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
const stickyHeaderIndices = [];
//处理header
if (ListHeaderComponent) {
if (stickyIndicesFromProps.has()) {
stickyHeaderIndices.push();
}
const element = React.isValidElement(ListHeaderComponent)
? (ListHeaderComponent)
: (<ListHeaderComponent/>);
cells.push(
<View
key="$header"
onLayout={this._onLayoutHeader}
style={inversionStyle}>
{element}
</View>,
);
}
//获取数据数量
const itemCount = this.props.getItemCount(data);
if (itemCount > 0) {
_usedIndexForKey = false;
const spacerKey = !horizontal ? 'height' : 'width';
//计算初始化末尾值,有初始化值为-1,无初始化值为默认9
const lastInitialIndex = this.props.initialScrollIndex
? -1
: this.props.initialNumToRender - 1;
const {first, last} = this.state;
//绘制初始化items
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
0,
lastInitialIndex,
inversionStyle,
);
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
//非0初始化(即:initialScrollIndex值不为0)
if (!isVirtualizationDisabled && first > lastInitialIndex + 1) {
let insertedStickySpacer = false;
if (stickyIndicesFromProps.size > 0) {
const stickyOffset = ListHeaderComponent ? 1 : 0;
//如果存在section,绘制出section和section中的内容,用多个空view代替
// See if there are any sticky headers in the virtualized space that we need to render.
for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) {
//...代码省略...
}
}
//不存在section,直接绘制一个空view
if (!insertedStickySpacer) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace = this._getFrameMetricsApprox(first).offset
- (initBlock.offset + initBlock.length);
cells.push(
<View key="$lead_spacer" style={{[spacerKey]: firstSpace}}/>,
);
}
}
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
firstAfterInitial,
last,
inversionStyle,
);
if (!this._hasWarned.keys && _usedIndexForKey) {
this._hasWarned.keys = true;
}
//last小于itemCount(本次刷新过程中不用显示的元素用一个空View代替)
if (!isVirtualizationDisabled && last < itemCount - 1) {
const lastFrame = this._getFrameMetricsApprox(last);
const end = this.props.getItemLayout
? itemCount - 1
: Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
const endFrame = this._getFrameMetricsApprox(end);
const tailSpacerLength =
endFrame.offset +
endFrame.length -
(lastFrame.offset + lastFrame.length);
cells.push(
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}}/>,
);
}
} else if (ListEmptyComponent) {//没有数据的话显示 空样式
const element = React.isValidElement(ListEmptyComponent) ? (
ListEmptyComponent
) : (
// $FlowFixMe
<ListEmptyComponent/>
);
cells.push(
<View
key="$empty"
onLayout={this._onLayoutEmpty}
style={inversionStyle}>
{element}
</View>,
);
}
//footer
if (ListFooterComponent) {
const element = React.isValidElement(ListFooterComponent)
? (ListFooterComponent)
: (<ListFooterCo mponent/>);
cells.push(
<View
key="$footer"
onLayout={this._onLayoutFooter}
style={inversionStyle}>
{element}
</View>,
);
}
//事件...
const scrollProps = {
...this.props,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout,
onScroll: this._onScroll,
onScrollBeginDrag: this._onScrollBeginDrag,
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollEnd: this._onMomentumScrollEnd,
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
stickyHeaderIndices,
};
if (inversionStyle) {
scrollProps.style = [inversionStyle, this.props.style];
}
//将列表元素添加到ScrollView中
const ret = React.cloneElement(
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(
scrollProps,
),
{
ref: this._captureScrollRef,
},
cells,
);
if (this.props.debug) {
return (
<View style={{flex: 1}}>
{ret}
{this._renderDebugOverlay()}
</View>
);
} else {
return ret;
}
其中有几个父组件传入的方法及属性需说明一下:
-
:获取列表内容数量;getItemCount
-
:用于避免动态测量内容尺寸的开销,如getItemLayout
getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )}
-
:开始时屏幕顶端的元素是列表中的第 initialScrollIndex 个元素, 而不是第一个元素;initialScrollIndex
-
:将数据源中的first—last之间的数据绑定到view上,添加到列表数据中。(VirtualizedList中的item组件为CellRenderer,其会调用renderItem方法绘制自定义的组件,此文不分析该部分)_pushCells
我们从itemCount > 0后开始分析:
- 从state中获取{first,last}。每次绘制就是绘制在数据源data中下标为first—last这部分数据。
- 添加0—lastInitialIndex元素。当initialScrollIndex为正整数,该部分不会添加任何元素;若未定义initialScrollIndex或为0的时候,默认添加0—9数据。
- 添加可见区域上方空View。当initialScrollIndex为正整数,将添加空白View代替可见区域上方的内容。
- 添加firstAfterInitial—last元素;当initialScrollIndex为正整数,该部分添加的为first—last数据;若未定义initialScrollIndex或为0的时候,添加的为10-last数据。
- 添加可见区域下方空View。若本次没有将之后的数据绘制完成,将添加空白View进行代替。
至此,render方法中的绘制流程已经完毕,但其实仅仅在之前绘制数据的基础上多绘制了<=10条数据,那么就引出两个问题,1、怎么进行下一次绘制?2、怎么计算first、last?
1、循环绘制
componentDidUpdate() {
this._scheduleCellsToRenderUpdate();
}
在每次刷新完成后会调用
_scheduleCellsToRenderUpdate
方法,该方法最终会调用
_updateCellsToRender
方法。
_updateCellsToRender = () => {
const {data, getItemCount, onEndReachedThreshold} = this.props;
const isVirtualizationDisabled = this._isVirtualizationDisabled();
this._updateViewableItems(data);
if (!data) {
return;
}
this.setState(state => {
let newState;
if (!isVirtualizationDisabled) {
// If we run this with bogus data, we'll force-render window {first: 0, last: 0},
// and wipe out the initialNumToRender rendered elements.
// So let's wait until the scroll view metrics have been set up. And until then,
// we will trust the initialNumToRender suggestion
if (this._scrollMetrics.visibleLength) {
// If we have a non-zero initialScrollIndex and run this before we've scrolled,
// we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex.
// So let's wait until we've scrolled the view to the right place. And until then,
// we will trust the initialScrollIndex suggestion.
if (!this.props.initialScrollIndex || this._scrollMetrics.offset) {
newState = computeWindowedRenderLimits(
this.props,
state,
this._getFrameMetricsApprox,
this._scrollMetrics,
);
}
}
} else {
const {contentLength, offset, visibleLength} = this._scrollMetrics;
const distanceFromEnd = contentLength - visibleLength - offset;
const renderAhead =
distanceFromEnd < onEndReachedThreshold * visibleLength
? this.props.maxToRenderPerBatch
: ;
newState = {
first: ,
last: Math.min(state.last + renderAhead, getItemCount(data) - ),
};
}
return newState;
});
};
在
_updateCellsToRender
中会调用setState方法更新状态。所以在每次绘制完成(状态更新完成)后,都会接着调用更新方法,所以形成了循环绘制的效果。理论上这种结构会造成无限循环,但是VirtualizedList是继承自PureComponent,所以当检测到状态未改变的时候就会终止更新。
控制循环绘制的是由Batchinator类控制的,该类位于Libraries/Interaction目录下,批量处理一个回调方法,值得学习。
2、计算first、last
在上述
_updateCellsToRender
方法中,调用了computeWindowedRenderLimits生成最新的first、last,该方法属于VirtualizeUtils类。它是根据优先级动态计算first和last,距离屏幕显示组件的数据越近,优先级越高。比如:当前页面显示的是10-20数据,根据优先级算法计算后为5-25。
总结
VirtualizedList是通过渲染空View的方法提升了性能,相对于ListView是通过渲染机制来进行优化的,但是还是基于ScrollView的,并没有进行组件复用,肯定还有很大的提升空间!
有错误请指出,多谢!