在前端的业务中,存在以下几种情况需要用到虚拟列表,旨在解决数据量庞大时浏览器渲染性能瓶颈。比如:
以上便是我在工作中常见的场景,还有很多情况,就不一一列举了。
虚拟列表也是可视化加载的一种,即只渲染可视容器中的内容。
假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psMg5uP4-1658805024111)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a51304d6b6c42e785c5e3ae3f21d99b~tplv-k3u1fbpfcp-watermark.image?)]
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
可视区域起始数据索引(startIndex)可视区域结束数据索引(endIndex)可视区域的数据,并渲染到页面中startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUhn7xMB-1658805024112)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5fca76f0393d468bbd530dbbecdf6b9f~tplv-k3u1fbpfcp-watermark.image?)]
<div class="virtual-list-container">
<div class="virtual-list-place">div>
<div class="virtual-list">
div>
div>
virtual-list-container 为可视区域的容器virtual-list-place 为容器内的占位,高度为总列表高度,用于形成滚动条virtual-list 为列表项的渲染区域接着,监听virtual-list-container的scroll事件,获取滚动位置scrollTop
可视区域高度固定,称之为screenHeight列表每项高度固定,称之为itemSize列表数据称之为listData当前滚动位置称之为scrollTop则可推算出:
listHeight = listData.length * itemSizevisibleCount = Math.ceil(screenHeight / itemSize)startIndex = Math.floor(scrollTop / itemSize)endIndex = startIndex + visibleCountvisibleData = listData.slice(startIndex,endIndex)当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。
startOffset = scrollTop - (scrollTop % itemSize);在之前的实现中,列表项的高度是固定的,因为高度固定,所以可以很轻易的获取列表项的整体高度以及滚动时的显示数据与对应的偏移量。而实际应用的时候,当列表中包含文本之类的可变内容,会导致列表项的高度并不相同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rZH5QFIz-1658805024112)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e96e61b783d046e8a5d758088e275be1~tplv-k3u1fbpfcp-watermark.image?)]
在虚拟列表中应用动态高度的解决方案一般有如下三种:
1.对组件属性
itemSize进行扩展,支持传递类型为数字、数组、函数
这种方式虽然有比较好的灵活度,但仅适用于可以预先知道或可以通过计算得知列表项高度的情况,依然无法解决列表项高度由内容撑开的情况。
2.将列表项
渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染至可视区域内。
由于预先渲染至屏幕外,再渲染至屏幕内,这导致渲染成本增加一倍,这对于数百万用户在低端移动设备上使用的产品来说是不切实际的。
3.以
预估高度先行渲染,然后获取真实高度并缓存。
这是我选择的实现方式,可以避免前两种方案的不足。
接下来,来看如何简易的实现:
定义组件属性itemHeight,用于接收预估高度
props: {
//预估高度
itemHeight:{
type:Number
}
}
定义positions,用于列表项渲染后存储每一项的高度以及位置信息,
this.positions = [
// {
// top:0,
// bottom:100,
// height:100
// }
];
由于需要在渲染完成后,获取列表每项的位置信息并缓存,所以使用钩子函数updated来实现:
updated(){
let nodes = this.$refs.items;
nodes.forEach((node)=>{
let rect = node.getBoundingClientRect();
let height = rect.height;
let index = +node.id.slice(1)
let oldHeight = this.positions[index].height;
let dValue = oldHeight - height;
//存在差值
if(dValue){
this.positions[index].bottom = this.positions[index].bottom - dValue;
this.positions[index].height = height;
for(let k = index + 1;k<this.positions.length; k++){
this.positions[k].top = this.positions[k-1].bottom;
this.positions[k].bottom = this.positions[k].bottom - dValue;
}
}
})
}
组件VirtualList:
{{item.value}}
开启itemAutoHeight,并且设置bufferScale比例来增加上下缓冲区大小保证加载的内容能撑满可视区域
{{item.value}}
在onReachBottom中做接口请求,这样的话既能通过分页优化,又能在加载的数据量大的时候采用虚拟列表方案
{{item.value}}
// 虚拟列表
npm i -S react-window
// 虚拟表格
npm i -S virtuallist-antd
// 虚拟列表
npm i -S vue-virtual-scroll-list
// 虚拟表格
npm i -S vxe-table