React Native组件之VirtualizedList

4,480 阅读7分钟

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,距离屏幕显示组件的数据越近,优先级越高。