天天看點

ReactNative源碼分析之VirtualizedList

  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 個元素, 而不是第一個元素;
  • _pushCells

    :将資料源中的first—last之間的資料綁定到view上,添加到清單資料中。(VirtualizedList中的item元件為CellRenderer,其會調用renderItem方法繪制自定義的元件,此文不分析該部分)

我們從itemCount > 0後開始分析:

  1. 從state中擷取{first,last}。每次繪制就是繪制在資料源data中下标為first—last這部分資料。
  2. 添加0—lastInitialIndex元素。當initialScrollIndex為正整數,該部分不會添加任何元素;若未定義initialScrollIndex或為0的時候,預設添加0—9資料。
  3. 添加可見區域上方空View。當initialScrollIndex為正整數,将添加空白View代替可見區域上方的内容。
  4. 添加firstAfterInitial—last元素;當initialScrollIndex為正整數,該部分添加的為first—last資料;若未定義initialScrollIndex或為0的時候,添加的為10-last資料。
  5. 添加可見區域下方空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的,并沒有進行元件複用,肯定還有很大的提升空間!

有錯誤請指出,多謝!

繼續閱讀