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的,并沒有進行元件複用,肯定還有很大的提升空間!
有錯誤請指出,多謝!