浅析虚拟列表高性能渲染大体量数据原理
在工作中,有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况,对于此,我们称这种列表叫做长列表。比如,在一些外汇交易系统中,前端会实时的展示用户的持仓情况(收益、亏损、手数等),此时对于用户的持仓列表一般是不能分页的。
在浅析如何利用时间分片高性能渲染十万级数据一文中,提到了可以使用时间分片的方式来对长列表进行渲染,但这种方式更适用于列表项的DOM结构十分简单的情况。本文会介绍使用虚拟列表的方式,来同时加载大量数据。
一、为什么需要使用虚拟列表
在实际的工作中,列表项必然不会仅仅只由一个li标签组成,必然是由复杂DOM节点组成的。那么可以想象的是,当列表项数过多并且列表项结构复杂的时候,同时渲染时,会在Recalculate Style和Layout阶段消耗大量的时间。
而虚拟列表就是解决这一问题的一种实现。
二、什么是虚拟列表
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。
假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。
说完首次加载,再分析一下当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项。
假设滚动发生,滚动条距顶部的位置为150px,则我们可得知在可见区域内的列表项为第4项至第13项。
三、实现
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
1、计算当前可视区域起始数据索引(startIndex)
2、计算当前可视区域结束数据索引(endIndex)
3、计算当前可视区域的数据,并渲染到页面中
4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上
由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:
<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
- 假定
可视区域
高度固定,称之为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)
当滚动后,由于渲染区域
相对于可视区域
已经发生了偏移,此时我需要获取一个偏移量startOffset
,通过样式控制将渲染区域
偏移至可视区域
中。
- 偏移量
startOffset
= scrollTop - (scrollTop % itemSize);
本以为虚拟列表是个啥好东西,没想到其实就是一个可视区域显示的问题,很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。更详细的内容就看这篇吧,讲的挺详细的:列表优化之虚拟列表:https://www.jianshu.com/p/39404c94dbd0。
其实我们墨天轮项目上在线文档那个模块 -文档预览的实现就用到了这一机制原理,比这个的交互还更为复杂(前预请求渲染、后预请求渲染、跳转等等),貌似我还没有写博客,以后有时间再补上吧。