React Native元件之VirtualizedList
React Native(簡稱RN)列表是基於ScrollView實現的,也就是可以滾動的,然而RN並沒有直接使用IOS或Android的原生列表元件,這是因為RN真正呼叫native程式碼的過程是非同步的,二Native的渲染要求必須同步渲染的。
在早期版本中,對於列表情況RN採用的是ListView元件,和Android一樣,早期的ListView元件效能是非常的差的,在後來的版本中,RN提供了系列用於提高列表元件效能的元件:FlatList和SectionList。FlatList和SectionList都是基於VirtualizedList實現的。
讀者可以在專案的“node_modules/react-native/Libraries/Lists/XXX”資料夾下找到相關的原始碼。一般來說,FlatList和SectionList已經能夠滿足常見的開發需求,僅當想獲得比FlatList 更高的靈活性(比如說在使用 immutable data 而不是普通陣列)的時候,才會應該考慮使用VirtualizedList。
VirtualizedList
VirtualizedList通過維護一個有限的渲染視窗(其中包含可見的元素),並將渲染視窗之外的元素全部用合適的定長空白空間代替的方式,極大的改善了記憶體消耗以及在有大量資料情況下的使用效能(類似於Android的ListView的介面複用機制)。
當一個元素離可視區太遠時,它的渲染的優先順序較低,否則就獲得一個較高的優先順序,VirtualizedList通過這種機制來提高列表的渲染效能。在使用VirtualizedList贏注意以下幾點:
- 當某行滑出渲染區域之外後,其內部狀態將不會保留,請確保在行元件以外的地方保留了資料。
- 本元件繼承自PureComponent而非通常的Component,這意味著如果其props在淺比較中是相等的,則不會重新渲染。所以請先檢查你的renderItem函式所依賴的props資料(包括data屬性以及可能用到的父元件的state),如果是一個引用型別(Object或者陣列都是引用型別),則需要先修改其引用地址(比如先複製到一個新的Object或者陣列中),然後再修改其值,否則介面很可能不會重新整理。
- 為了優化記憶體佔用同時保持滑動的流暢,列表內容會在螢幕外非同步繪製。這意味著如果使用者滑動的速度超過渲染的速度,則會先看到空白的內容。
- 預設情況下每行都需要提供一個不重複的key屬性,開發者可以提供一個keyExtractor函式來生成key(不然會給出黃色的警告資訊)。
屬性
由於VirtualizedList是FlatList和SectionList的父元件,所以VirtualizedList提供的屬性,FlatList和SectionList都能夠找到。
- data?: any
預設的函式獲取器,假設它是一個數組型別(Array<{key: string}>),但是可以通過重寫getItem、getItemCount、keyExtractor 來處理任何型別的可索引資料。 - debug?: ?boolean
開啟額外的日誌和視覺覆蓋功能來協助除錯,但是開啟會影響效能。 - disableVirtualization: boolean
已過時: Virtualization 提供了顯著的效能和記憶體優化,並且完全解除安裝了位於可視區之外的 react 例項。 - extraData?: any
標記屬性,用來告訴列表需要重新渲染(實現了PureComponent)。如果有 data 屬性之外的資料引用,就需要把它列在這裡,並把它當成不可變的。 - getItem: (data: any, index: number) => ?Item
通用的獲取器,用來從任意型別的資料塊中獲取一個元素。 - getItemCount: (data: any) => number
用來決定資料塊中一共有多少元素。 - getItemLayout?: (data: any, index: number)
getItemLayout是一個可選的優化,用於避免動態測量內容尺寸的開銷,不過前提是你可以提前知道內容的高度。例如,如果行高是固定的,那麼getItemLayout用起來就既高效又簡單。 - horizontal?: ?boolean
設定為true則變為水平佈局模式。 - initialNumToRender: number
首批應該渲染的元素數量。注意:為了響應“滾動到頂部”這個事件並最優化其效能,這些元素將作為視窗渲染的一部分,永遠不會被解除安裝。 - keyExtractor: (item: Item, index: number)
此函式用於為給定的item生成一個不重複的key。Key的作用是使React能夠區分同類元素的不同個體,以便在重新整理時能夠確定其變化的位置,減少重新渲染的開銷。 - maxToRenderPerBatch: number
每批增量渲染可渲染的最大數量。能立即渲染出的元素數量越多,填充速率就越快,但是響應性可能會有一些損失,因為每個被渲染的元素都可能參與或干擾對按鈕點選事件或其他事件的響應。 - onEndReached?: ?(info: {distanceFromEnd: number})
當列表被滾動到距離內容最底部不足 onEndReachedThreshold 的距離時呼叫。 - onEndReachedThreshold?: ?number
決定當距離內容最底部還有多遠時觸發 onEndReached 回撥,注意此引數是一個比值而非畫素單位。 - onLayout?: ?Function
當元件掛載或者佈局變化的時候呼叫,同View的onLayout。 - onRefresh?: ?Function
如果設定了此選項,則會在列表頭部新增一個標準的RefreshControl控制元件,以便實現“下拉重新整理”的功能。 - onViewableItemsChanged
當列表中行的可見性發生變化時,就會呼叫這個函式。可見性設定見viewabilityConfig。 - refreshing?: ?boolean
當等待資料進行更新時,將這個屬性設定為true。 - removeClippedSubviews?: boolean
一個將“剪裁子檢視”(clipped subviews)從檢視層級中刪除的本地優化,為的是減輕渲染系統的工作負擔。但是這些被剪裁掉的子檢視依然保留在記憶體中,所以它們所佔的儲存空間沒有被釋放,內部狀態也都保留了下來。 - renderItem: (info: {item: Item, index: number})
根據行資料data渲染每一行的元件檢視。 - renderScrollComponent: (props: Object)
渲染一個自定義的滾動元件,比如說這個元件有一種不同的重新整理控制方式。 - updateCellsBatchingPeriod: number
具有較低渲染優先順序的元素(比如那些離螢幕相當遠的元素)的渲染批次之間的時間間隔。與maxToRenderPerBatch具有相同的目的,都是為了在渲染速率和響應性之間獲得一個平衡。 - windowSize: number
設定可視區外最大能被渲染的元素的數量,以可視區的長度為單位。將windowSize設定為一個較小值,能有減小記憶體消耗並提高效能,但是當你快速滾動列表時,遇到尚未渲染的內容的機率會增大,而這些尚未渲染的內容會暫時性地被空白區塊所替代。
原始碼分析
VirtualizedList執行的流程如下:
- 每次新增繪製item的最大數量為10,迴圈繪製(預設以10為單位累加繪製);
- 首先繪製顯示在螢幕中的items,再根據優先順序迴圈繪製螢幕上顯示items相近的資料,直至繪製完成;
- 每次繪製過程中,所有不需要繪製的元素用空View代替;
迴圈繪製
迴圈繪製的載入方法為_scheduleCellsToRenderUpdate()。
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
: 0;
newState = {
first: 0,
last: Math.min(state.last + renderAhead, getItemCount(data) - 1),
};
}
return newState;
});
};
在_updateCellsToRender中會呼叫setState方法更新狀態。所以在每次繪製完成(狀態更新完成)後,都會接著呼叫更新方法,所以形成了迴圈繪製的效果。理論上這種結構會造成無限迴圈,但是VirtualizedList是繼承自PureComponent,所以當檢測到狀態未改變的時候就會終止更新。
在上述_updateCellsToRender方法中,呼叫了computeWindowedRenderLimits生成最新的first、last,該方法屬於VirtualizeUtils類。它是根據優先順序動態計算first和last,距離螢幕顯示元件的資料越近,優先順序越高。