React Native組件之VirtualizedList

React Native(簡稱RN)列表是基於ScrollView實現的,也就是能夠滾動的,然而RN並無直接使用IOS或Android的原生列表組件,這是由於RN真正調用native代碼的過程是異步的,二Native的渲染要求必須同步渲染的。node

在早期版本中,對於列表狀況RN採用的是ListView組件,和Android同樣,早期的ListView組件性能是很是的差的,在後來的版本中,RN提供了系列用於提升列表組件性能的組件:FlatList和SectionList。FlatList和SectionList都是基於VirtualizedList實現的。react

讀者能夠在項目的「node_modules/react-native/Libraries/Lists/XXX」文件夾下找到相關的源碼。通常來講,FlatList和SectionList已經可以知足常見的開發需求,僅當想得到比FlatList 更高的靈活性(好比說在使用 immutable data 而不是普通數組)的時候,纔會應該考慮使用VirtualizedList。react-native

VirtualizedList

VirtualizedList經過維護一個有限的渲染窗口(其中包含可見的元素),並將渲染窗口以外的元素所有用合適的定長空白空間代替的方式,極大的改善了內存消耗以及在有大量數據狀況下的使用性能(相似於Android的ListView的界面複用機制)。數組

當一個元素離可視區太遠時,它的渲染的優先級較低,不然就得到一個較高的優先級,VirtualizedList經過這種機制來提升列表的渲染性能。在使用VirtualizedList贏注意如下幾點:bash

  • 當某行滑出渲染區域以外後,其內部狀態將不會保留,請確保在行組件之外的地方保留了數據。
  • 本組件繼承自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,距離屏幕顯示組件的數據越近,優先級越高。

相關文章
相關標籤/搜索