Skip to content

VirtualizedList/FlatList工作原理及实现源码 #142

@smallnewer

Description

@smallnewer

本文主要阐述VirtualizedList的工作原理。

VirtualizedList诞生是因为ReactNative中的大列表体验不够好。最早的ScrollView会一次把所有元素渲染,所以问题有:初始化很慢;DIFF开销大;视野外的View持续占用内存。ListView不再初始化时创建所有,当滚动时逐步创建,但其仍然不会释放,所以:初始化快了;但DIFF开销仍然大、内存仍然会无限增长。VirtualizedList是想要更彻底的解决大列表性能问题,但结果仍差强人意。不过作为官方目前的方案,只有了解它才能改进它。

阐述源码是一件枯燥的事,更重要的是其思路和工作原理。便可以进一步找出优化点。
VirtualizedList的改进思路很简单,只创建视野附近区域的Cell,超出就释放;它有三种情况会优先创建:优先靠近当前视野的Cell、优先创建滚动朝向的Cell、优先顶部和底部的Cell。
这个思路的实现方案,仍然没有逃开React的核心机制,利用setState->DiffTree->更新渲染。
之所以用逃开这个词,是因为在ReactNative里这个过程有几个开销:Diff、消息生成、消息跨线程传递。这几个开销无疑对列表性能影响可观(但本文先抛开这个不提了)。底层还是使用的ScrollView。

它根据输入的data数组,生成了如下的children结构。

2

当滚动时,更新机制会产生新的vdom tree,通常是first-last这个区间会变化。

... ...
first=1 first=3
2 4
3 5
4 6
last=5 last=7
... ...

在diff之后,就会生成一些删除、更新、创建的指令。

其更新流程大体如下。触发更新有几个事件。这几个事件都很重要。

1

这其中的一个关键就是,需要尽可能的、提前的、准确的拿到所有数据对应cell的坐标信息。因为在很多地方的计算都依赖于此(比如在第一屏就要知道最后一个数据的位置,为了满足scrollToEnd)

VirtualizedList的解决办法是:在onCellLayout时,存储准确的Cell坐标在this._frames[key]上。而任意数据对应cell的坐标时:先通过this._frames取准确的,如果没有,再尝试通过getItemLayout获取,如果没有指定getItemLayout,就进入估算逻辑:通过_averageCellLength*index来估算(_averageCellLength则是onCellLayout触发时更新,这种情况下,它会假定每个cell都是固定高的。)当然最后可能会有两种错误:

估算错误和取不到坐标(比如onCellLayout没触发或某些时机还没来得及触发onCellLayout)。估算错误那将错就错了,只能容忍。取不到的情况,就会退化为所有Cell都创建,会异步分批进行。(在此期间会发生什么我也没确认,估计会有问题的)

基于VirtualizedList衍生出两个业务常用组件:FlatList和SectionList,这个没什么可说的。关于Section部分的实现我先略过了,它不是性能的瓶颈。

最后说下VirtualizedList的问题吧,它在滚动时没有复用Cell,仍然大量的创建,因此滚动快速时,白屏时不可避免的。但即便复用Cell,由于React Native采用的时消息通信机制,也会有旧Cell闪烁的问题。归根到底,是因为滚动处理和渲染位于两个线程,非同步操作导致的。剩下的优化思考另起一篇吧。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions