虚拟列表
面试题:一次性给你 10000 条数据,前端怎么渲染到页面上?
长列表常见的 3 种处理方式:
- 懒加载
- 时间分片
- 虚拟列表
懒加载
懒加载的原理在于:只有视口内的内容会被加载,其他内容在用户滚动到视口时才会被加载。这可以显著减少初次加载的时间,提高页面响应速度。这样的好处在于:
- 节省带宽
- 提升用户体验
懒加载的缺点:懒加载只能应对数据不是太多的情况。如果列表项有几万甚至几十万项,最终会有对应数量的 DOM 存在于页面上,严重降低页面性能。
时间分片
时间分片的本质是通过 requestAnimationFrame,由浏览器来决定回调函数的执行时机。大量的数据会被分多次渲染,每次渲染对应一个片段。在每个片段中处理定量的数据后,会将主线程还给浏览器,从而实现快速呈现页面内容给用户。
时间分片的缺点:
- 效率低
- 不直观
- 性能差
总结:无论是懒加载还是时间分片,最终都是将完整数量的列表项渲染出来,这在面对列表项非常非常多的时候,页面性能是比较低的。
虚拟列表
原理:设置一个可视区域,然后用户在滚动列表的时候,本质上是动态修改可视区域里面的内容。
例如,一开始渲染前面 5 个项目

之后用户进行滚动,就会动态的修改可视区域里面的内容,如下图所示:

也就是说,始终渲染的只有可视区的那 5 个项目,这样能够极大的保障页面性能。
实现:实现定高的虚拟列表,这里所指的定高是说列表项的每一项高度相同
涉及到的信息:
- 可视区域起始数据索引(startIndex)
- 可视区域结束数据索引(endIndex)
- 可视区域的数据
- 整个列表中的偏移位置 startOffset
如下图所示:

接下来整个虚拟列表的设计如下:
<div class="infinite-list-container">
<!-- 该元素高度为总列表的高度,目的是为了形成滚动 -->
<div class="infinite-list-phantom"></div>
<!-- 该元素为可视区域,里面就是一个一个列表项 -->
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
- infinite-list-container:可视区域的容器
- infinite-list-phantom:容器内的占位,高度为总列表高度,用于形成滚动条
- infinite-list:列表项的渲染区域
如下图所示:

接下来监听 infinite-list-container 的 scroll 事件,获取滚动位置的 scrollTop,因为回头需要设置 list 向下位移的距离
- 假定可视区域高度固定,称之为 screenHeight
- 假定列表每项高度固定,称之为 itemSize
- 假定列表数据称之为 listData(就是很多的列表数据,几万项、几十万项列表数据)
- 假定当前滚动位置称之为 scrollTop
那么我们能够计算出这么一些信息:
- 列表总高度 :listHeight = listData.length * itemSize
- 可显示的列表项数 : visibleCount = Math.ceil(screenHeight / itemSize)
- 数据的起始索引: startIndex = Math.floor(scrollTop / itemSize)
- 数据的结束索引: endIndex = startIndex + visibleCount
- 列表显示数据为: visibleData = listData.slice(startIndex, endIndex)
当发生滚动后,由于渲染区域相对于可视区域发生了偏移,因此我们需要计算出这个偏移量,然后使用 transform 重新偏移回可视区域。
偏移量的计算:startOffset = scrollTop - (scrollTop % itemSize)
思考🤔:为什么要减去 scrollTop % itemSize ?
答案:之所以要减去 scrollTop % itemSize,是为了将 scrollTop 调整到一个与 itemSize 对齐的位置,避免显示不完整的列表项。

实战演练:实现定高的虚拟列表
定高的虚拟列表存在一些问题,或者说可以优化的地方:
- 动态高度
- 白屏现象
- 滚动事件触发频率过高
-EOF-