📖 阅读分享
维持死刑制度的既不是国民也不是国家,而是杀人犯自己! — 《消失的13级台阶》 [日] 高野和明
虚拟滚动: 复用你的DOM元素,根据用户的滚动方向移除你视口的一个元素。
当列表需要展示上千上万级别甚至是趋向于无限的数据时,DOM元素的堆积会导致浏览器渲染的性能降低,导致用户体验降低甚至出现页面假死状态。这个时候使用虚拟滚动的技术,可以让DOM数量维持在固定的数量,从而解决以上问题。但对于少量数据的场景下,使用传统的加载方式会优于虚拟滚动,虚拟滚动的核心是监听滚动事件去进行复杂的逻辑计算,两者衡量之下,少量的数据的DOM消耗要远小于虚拟滚动的计算
已有虚拟滚动技术的应用: Google Music, Twitter, Facebook。
文章接下来会讲述 固定高度的虚拟滚动 到 动态高度的虚拟滚动是如何实现,技术的难点是什么,并以React的框架为基础,用代码的形式去介绍算法核心点。
固定高度
为了实现跟传统列表加载的体验一样,虚拟滚动需要做到以下几点:
- 计算容器能承载的DOM元素容量
- 模拟滚动高度
- 实时计算显示的元素
1. 承载容量
在计算机领域中有一个名词叫视口,代表当前可见的计算机图形区域。 在虚拟滚动技术中,视口是承载DOM元素的容器,超出该容器的DOM元素是不可见的,需要通过滚动来展示,如下图,白色部分的高度就是容器的高度,蓝色的DOM元素就是用户可见的元素。
假设容器高度为H, 单个DOM元素的高度为DH,那么容器可见的数量为 VISIBLE_COUNT = H / DH。但在实际滚动过程中,仅仅渲染可见的数量是不够的,因为滚动过程的实时计算会导致浏览器的渲染不够及时,可能会出现留白的情况,所以我们需要在上下两边都加上一个缓存用的DOM元素。 假设BUFFER_SIZE是3, 按照上图所示,则真正需要渲染的DOM的数量是 4 + 3 * 2 = 10个, ITEM_COUNT = ⌈H / DH⌉ + BUFFER_SIZE * 2;
为了更方便去过滤该渲染的DOM元素,那么我们设置两个变量去筛选,firstItem和lastItem。
useLayoutEffect(() => {
ELEMENT_HEIGHT = outerHeight(itemRef.current);
const containerHeight = containerRef.current?.clientHeight ?? 0;
VISIBLE_COUNT = Math.ceil(containerHeight / ELEMENT_HEIGHT);
setLastItem(VISIBLE_COUNT + BUFFER_SIZE);
}, [])
2. 模拟滚动高度
因为要让用户感受跟传统滚动一样的效果和体验,然而有限的DOM元素不足以撑开上千个数据的列表,所以我们要用css去帮忙撑开,撑开的方式有两种:
- 通过设置容器的padding-bottom,让其出现滚动条
- 设置一个哨兵元素,并设置哨兵元素的translateY的值,同样能使容器出现滚动条。
💡PS:哨兵在现实中,是用来解决国家之间的边界问题,不直接参与生产活动。在计算机领域也是为了处理边界的问题,可以减少很多边界问题的判断,降低代码复杂性。
我这边推荐第二种方式,理由是:浏览器的重排与重绘。特别是后面介绍动态高度滚动的时候,会不断计算可滚动的高度,这对于性能来说也是一些优化,既然能优化,当然要优化得彻底一些。
因为每个DOM元素的高度是固定的,所以只要每次列表有变化的时候,再对这个高度进行计算就可以,scrollHeight = list.length * ELEMENT_HEIGHT;
<div onScroll={scroll} ref={containerRef} className={styles.container}>
// 哨兵,用于撑开滚动高度
<div className={styles.sentry} style={
{ transform: `translateY(${scrollHeight}px)` }} ></div>
// 可以先忽略下方的代码
{
visibleList.map((item, idx) =>
<div key={idx} style={
{transform: `translateY(${item.scrollY}px)`}} className={styles.wrapItem} >
<Item ref={itemRef} item={item} />
</div>
)
}
</div>
useLayoutEffect(() => {
// 可先忽略这段代码
list.forEach((item, idx) => {
item.scrollY = idx * ELEMENT_HEIGHT;
})
// list变化时,更新scrollHeight的值
setScrollHeight(list.length * ELEMENT_HEIGHT);
}, [list]);
3. 实时计算显示的元素
如何正确显示当前已滚动高度所对应的元素呢?这是本节所要解决的问题,也是整个虚拟滚动技术(固定或动态)的核心。
在固定高度的情况下,其实在加载list的时候,已经确定每个元素的位置,只要像哨兵一样,设置