天天看点

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的,并没有进行组件复用,肯定还有很大的提升空间!

有错误请指出,多谢!

继续阅读