唔~,好久没有更新博文了,一年前的这个时候用 Vue 实现过一版虚拟列表,不过现在已经忘光光了,故写一篇博文帮忙回忆与巩固一下虚拟列表的知识。
前言
本文对虚拟列表出现的原因与解决的问题不再介绍,着重讲解虚拟列表实现的思路以及原理。
技术栈
预览链接
问题分析
基本思路
虚拟列表的基本结构:
- 容器层(
container
):整个虚拟列表的包裹层,滚动事件是在这个包裹层出发的。
- 幻影层(
phantom
):容器层的子元素,用于撑开整个容器,获得原生的滚动条展示,不渲染内容。
- 内容层(
content
):容器层的子元素,用于渲染列表内容,列表元素动态变化。
虚拟列表的滚动效果实现:
首先我们知道,虚拟列表的视口的渲染元素是动态增减的,那么其平滑的滚动效果是如何出现的?这里有一个误区需要注意,虚拟列表的滚动效果就是原生的滚动效果,虚拟列表并不参与scrollTop
属性的设置,而是通过CSS
的transform
属性来实现的。
这里有两个基本点需要了解
- 虚拟列表对小于等于元素高度的滚动是直接体现的原生的滚动效果。
- 虚拟列表对大于元素高度的滚动是通过
CSS
的transform
属性来实现的,具体体现为元素消失在视口的时候,会对内容层的transform
属性增减一个元素的高度,实现内容与滚动的一致性。
细节分析
- 容器高度分析
- 容器层的高度:容器层的高度由幻影层的高度撑开,幻影层的高度依赖于需要渲染数据的总量与高度。
- 渲染区域的高度:即视口高度,由元素的
clientHeight
获取。
- 渲染的数据的数量
- 渲染的数据的数量:渲染的数据的数量由渲染区域的高度与元素的高度计算得出。
- 总数据的数量:即数据的长度。
- 可视数据索引的计算方式
- 开始索引:开始索引的计算方式为
Math.floor(scrollTop / itemHeight)
。
- 结束索引:开始索引加上可视渲染数据的个数。
what can i say, 多说无益,下面直接上代码。
代码实现
虚拟列表的基本结构
首先搭建虚拟列表的基本结构
声名需要用到的变量
处理元素渲染的副作用
监听滚动事件
完整代码
对不定高元素的渲染的处理
在上面的虚拟列表的实现中我们假定了列表项的高度是固定的,但在很多场景中,由于元素的内容高度与宽度不定,可能会造成列表元素的高度的不一致性,这需要一种能支持动态高度的虚拟列表。
实现动态虚拟列表的难点,在于如何正确的计算起始索引与结束索引并正确的展示元素。
这里我们为了实现一个动态的虚拟列表,采用现行用预估高度渲染,然后渲染后动态更新高度的手段。
这里核心的一点在于,在滚动下滚的时候动态更新高度数组,上滚的时候就可以正确的计算出起始索引与结束索引,从而正常展示元素。下面上代码
获取实际渲染元素列表的 DOM 实例,用于动态更新元素的高度
维护positions
数组,并初始化。
每次组件更新的时候都需要更新整个位置数组的对象
对滚动事件的处理
完整代码
对滚动过快白屏现象的处理
解决方法为上下都渲染一个缓冲区,防止滚动过快白屏。
定义上下缓冲区的大小。
由于顶部多渲染了一些元素,所以整体 2D 位移的值需要减小
总结
至此,我们完成了一个较为完备的虚拟列表的实现。支持动态高度并能解决滚动过快的白屏问题。
但是对于图片类渲染后决定高度的情况我们没有优化,可以考虑后续来继续解决,这里有两种解决方案:
- 在图片初始化的时候就固定图片的信息,固定宽高渲染,然后渲染。
- 根据
ResizeObserver
对象或者img
元素的onload
事件来监听图片的加载,然后更新高度数据。
另外频繁触发滚动事件可能对性能有些损耗,我们可以考虑采用节流函数来控制scroll
事件的执行次数,详情查看 Demo4