虚拟列表实现原理

“ 之前在做项目时遇到性能优化场景,当不断的增大列表行数或者列数时,会明显的感觉到卡顿的问题;于是就延展出虚拟列表的使用。如果中间说的有问题,欢迎在评论区留言指出。”

1. 虚拟列表的定义

为什么会有虚拟列表的产生?
拿table组件来举例子,创建一个table表格其实需要的成本是很高的,因为table表格中有很多的dom元素,而创建和渲染dom元素花费的时间成本是很高的,如果当我们table表格横向和纵向超过100条的时候会出现明显的卡顿,如果要完全的等待table渲染完的话,这种情况是不可取的,十分的影响用户体验。
因此对于上面的问题衍生出:可视区域进行渲染,这样可以提升初次渲染性能。
其实虚拟列表指的就是可视区域渲染的列表,归纳为两个概念:
(1)可视区域:可视区域就是可见区域,比如列表高度500px,可视区域高度就是500px,并且在可视区域内右侧是有滚动条滚动的;
(2)可滚动区域:假如有100条数据,每条数据项的高度是50px,那么可滚动区域的高度就是:数据条数 * 数据项高度,即100 * 50。随着用户改变滚动条时,此时可以看见可视区域内容的变更。
左边是普通滚动列表,右边是虚拟滚动列表
左边是普通滚动列表,右边是虚拟滚动列表

上图左边就是普通的滚动列表,右边是虚拟滚动列表,从图中我们就可以看出它们之间的差异:左边普通滚动列表它是把所有的dom一同渲染出来了;而右边虚拟滚动列表只是将可视区域的dom渲染出来,然后将右侧滚动条进行了一个变化。

2. 虚拟列表的实现思路

在上一章中介绍了虚拟列表的基本概念,下面我们就来介绍一下虚拟列表的实现,光讲概念感觉会一头雾水,首先看看下面这张图:
虚拟列表实现原理
虚拟列表实现原理

从上面图片中我们可以看到:
(1)用户实际每次能看到的元素内容只有元素7-元素14(即每次能看到8个元素);
(2)startIndex表示当前可视区域起始数据(元素7);
(3)endIndex表示当前可视区域结束数据(元素14);
(4)需要每次计算出当前可见区域的数据,并将其dom元素渲染到页面中;
(5)需要计算出startIndex对应的数据在列表中的偏移位置startOffset(元素1-元素6向上滑动隐藏距离,需要使用到scrollTop方法),并设置在列表中;
(6)以及计算当前的位置currentIndex。

虚拟列表的实现又分为静态动态的,所谓静态的就是高度是固定的,动态自然我们就知道是高度不固定的。

3. 虚拟列表之高度固定

  1. 具体步骤:

(1)list列表一共有多少条数据:TOTAL

(2)当前屏幕可视区域:HEIGHT

(3)list列表中每个元素固定高度:ITEM_HEIGHT

(4)list列表总的高度:TOTAL * ITEM_HEIGHT

(5)可视区域范围内展示的元素个数:Math.ceil(HEIGHT/ ITEM_HEIGHT),在这里使用的是ceil方法,采用的是向上取整,因为比如当前有一个元素项的一部分内容已经暴露在可视区域,这个时候是需要将当前项进行渲染的;

(6)当前位置currentIndex = Math.floor(scrollTop / ITEM_HEIGHT),这里采用向下取整的原因是:比如从0元素到1元素的过渡,此时还是没有过渡到1元素上,因此需要采用向下取整方法。

  1. 在定义好上面变量之后,由于是在滑动时需要不断的进行更新对应可视区域内的dom元素,因此我们需要监听可视区域容器的onScorll滚动事件,以此来更新startIndex和endIndex。

  2. 关于布局:

(1)首先需要有一个可视区域的容器virtualListBox,它是可滚动的,因此会有overflow-y: auto; 属性;

(2)接着需要一个真实还原list列表的高度的容器,能让出现的滚动条正常模拟滚动,该容器为virtualListPhantom,它的高度为list列表总高度,并且由于列表中的元素需要相对于它定位,因此它需要具有position: relative;属性;

(3)最后就是list列表中的每个元素定位是相对于virtualListPhantom容器的,因此它需要具有的属性是position: absolute;属性,并且它的top属性是根据当前index索引 * ITEM_HEIGHT来计算的。

  1. 在大致知道上面需要计算的变量后,接着就来举一个简单的例子:假设每个列表项的高度ITEM_HEIGHT都是100px,可视区域的高度HEIGHT为1000px,列表总条数TOTAL为1000条。具体的代码和注释在下面这个链接中,感兴趣的可以去看看。

固定高度虚拟列表线上demo演示

  1. 对于虚拟列表元素的渲染的原理,我总结成了下面一幅图,如下所示:
    关于虚拟列表渲染的原理
    关于虚拟列表渲染的原理

(1)其实从0-3元素过渡到1-4元素时,当startIndex为1时,此时页面触发重新渲染,我们眼睛是看不见页面发生了重新渲染的;原因在于0-3元素向上滚动到1-4元素时,滚动的内容和重绘的内容发生重叠,使得我们眼睛感受不到页面发生了变化,所以看着页面也是很流畅的。(如果你还是没有理解的话,如果你做过轮播图,知道它的原理,那么可以参照一下轮播图滚动衔接的原理。)

(2)在上面中也提到了key值的问题,其实如果只是单纯的展示列表,那么key值相同时可以减少页面中dom的重新渲染,但是如果对于需要操作列表元素的删除和修改时,key相同需要更加小心,因为可能会带来意想不到的bug。

(3)其实对于上面还存在一个问题,就是当我们快速上滑或者下滑时,列表会出现闪烁的现象,其原因是因为此时浏览器还未来得及渲染空白部分导致的闪频问题,解决:使用bufferSize缓冲过渡来不及渲染问题,其主要的代码在于starIndex之前和endIndex之后添加bufferSize,如下所示:

1
2
startIndex = Math.max(currentIndex - BUFFER_SIZE, 0); // 头部缓冲
endIndex = Math.min(startIndex + LIMIT + BUFFER_SIZE, TOTAL - 1); // 尾部缓冲

4. 虚拟列表之高度不固定

上面已经介绍了高度固定的虚拟列表,接下来我们来简单介绍高度不固定时的原理,当然在这里只讨论比较简单demo。

对于动态高度实现的思路和高度固定实现大同小异,不同之处在于我们需要缓存列表项位置的信息,那么大家先想想该如何拿到列表项的精准高度呢?

在实际开发中,有的列表项可能只有一行文本,有的列表项可能存在多行文本,因此我们需要基于项目实际情况考虑,可以取列表项前15个的平均高度作为预估的高度ESTIMATED_ITEM_HEIGHT,如果想提高精准度,那么可以扩大范围取值。

对于上面情况还会出现列表中存在图片的情况,但是一般我们的图片都是从cdn或者其他接口请求返回的,此时并不能保证在列表项组件挂载时图片渲染上了,那此时的列表项高度就会出现误差,用户在滚动时会出现列表元素1中的图片和元素列表2中的文字重叠的问题。

解决:监听列表项大小变化即可取得正确高度,此时可以考虑ResizeObserver接口,它可以监听元素内容区域或者边界框的改变,该接口可以参考下面这篇文章链接:

ResizeObserver使用

特别感谢,参考文章:

前端虚拟列表的实现原理

浅说虚拟列表的实现原理