虚拟列表

概念

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。当发生滚动时,我们再动态计算需要渲染的部分数据来达到对用户无感知的情况下只渲染部分内容来降低渲染压力的方法。

实现

我们可以将之前需要全部渲染的数据总高度分为五大部分,分别是如下图的上占位区,上缓冲区,屏幕视口,下缓存区以及下占位区。上下占位区的存在是为了保证总高度的一致性,防止渲染部分商品带来的高度塌陷导致的滚动高度变化问题,而上下缓冲区和屏幕视口就是我们每次实际要渲染的数据的部分,缓冲区的存在是为了每次多渲染一部分商品来减轻用户滑动足够快的时候不会出现看到短暂渲染白屏的情况。

实现步骤:

  1. 每一个商品在初始化的时候计算出商品到上缓冲区顶部的距离(即商品渲染距离)
1
2
3
4
5
6
7
8
9
10
11
12
// 拓展数据并给每一项要渲染的item添加一个distance的属性,这个distance即为到上缓冲区顶部的渲染距离
const expendDistanceToTopCache = (data) => {
let totalHeight = CACHE_HEIGHT; // 上缓冲区的高度
const { rowGap = 10 } = this.props;
const length = data?.length;
return data.forEach((item, index) => {
// 每一项的渲染高度 = 上一个item到上缓冲区顶部的高度 + 当前项目的高度 + 行间距高度
totalHeight += (item?.height ?? 0) + ( (index !== length - 1) ? rowGap : 0); // rowGap为每一行的间距
item.distance = totalHeight;
return item;
});
}

获取初始化需要渲染数据的索引区间,并确定初始化时候的渲染数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 2.1 计算初始化时候的渲染数据的开始索引值
const startRenderIdx = 0 // 初始化时候的渲染数据的开始索引为0
// 2.2 计算原始数据的总高度
dataHeight = (data?.[data.length - 1]?.distance) ?? 0; // 原始数据的总高度 = 原数据最后一项的渲染高度
// 2.3 计算渲染高度
const windowHeight = Dimensions.get('window').height ?? 0; // 视口的高度
const CACHE_HEIGHT = 3500; // 上下缓冲区的高度
const renderDataHeight = windowHeight + 2 * CACHE_HEIGHT; // 渲染高度 = 视口的高度 + 2 * 缓冲区高度(上缓冲区高度 + 下缓冲区高度)
// 2.4. 计算初始化时候的渲染数据的结束索引值
const endRenderIdx = data?.findIndex((itemData) => itemData?.distance >= renderDataHeight); // 初始化时候的结束索引 = 数据中第一个大于渲染高度的商品的索引
// 2.5 计算初始化时候的要渲染数据部分
const renderData = data.slice(startRenderIdx, endRenderIdx); // 初始化时候的要渲染数据部分
// 2.6 确定上下占位区的高度
const cacheTopHeight = (colData?.[startRenderIdx - 1]?.distance) ?? 0; // 首个渲染item的上一个的distance距离或者0
const cacheBottomHeight = Math.max(0, (dataHeight - ((data?.[endRenderIdx - 1]?.distance) ?? 0))); // 下缓冲的距离 = 总数据高度 - 最后一项渲染项高度

滚动的时候动态执行如2中的6项工作,根据每次的滚动高度来确定渲染数据的索引区间,并确定要渲染的数据, 不同之处在于增加了滚动距离所以计算开始索引和结束索引部分不太一样,其他都是基本一致的,只要动态进行如此的计算过程即可

1
2
3
4
5
6
7
// 滚动时的渲染数据的开始索引 = 第一个渲染距离 > scrollOffset(滚动距离)- CACHE_HEIGHT(上渲染区高度) 的数据项所在原始数组的索引
const startRenderIdx = data?.findIndex((itemData) => itemData?.distance >= scrollOffset - CACHE_HEIGHT);
if (startRenderIdx !== -1){
const batchEndHeight = ((data?.[startRenderIdx]?.distance) ?? 0) + renderDataHeight;
// 滚动时候渲染数据的结束索引项 = 原数据项中第一个渲染距离 > 当前批次的开始渲染索引项的渲染距离 + 每次必须的渲染高度的数据项在原始数组的索引
const endRenderIdx = data?.findIndex((itemData) => itemData?.distance >= batchEndHeight);
}