前言
前端在实现列表大量数据展示时,需要考虑到性能问题,一次性加载全部的数据,必然出现渲染时间过长导致页面卡顿的现象,过去我常用懒加载的方式实现数据的按需展示,现看到有博主介绍使用虚拟列表
去实现,在这里记录一下
什么是虚拟列表
虚拟列表是一种只对可见区域
中数据进行渲染,对不可见区域
中数据不渲染或部分渲染的技术,也是实现数据按需展示的一种方式,能大大提高渲染的性能
假设有1万条列表数据需要同时渲染,设我们屏幕可见区域的高度为500px,每条列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可
这是首次加载的情况,当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项
假设滚动发生,滚动条距顶部的位置为150px,则我们可得知在可见区域内的列表项为第4项至第13项
实现
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除
设如下几个值:
- startIndex - 当前可视区域起始数据索引
- endIndex - 当前可视区域结束数据索引
- startOffset - 数据展示列表在父盒子中的偏移量
我们要做以下几个步骤:
- 计算得到起始数据索引startIndex
- 计算得到结束数据索引endIndex
- 计算得到应该在当前可视区域内展示的数据,并渲染到页面中
- 计算偏移量startOffset并设置到数据展示列表上
dom结构如下
<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
- 假定列表每项高度固定,称之为itemHeight
- 假定列表数据称之为listData
- 假定当前滚动位置称之为scrollTop
则可推算出:
- 列表总高度listHeight = listData.length * itemHeight
- 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemHeight)
- 数据的起始索引startIndex = Math.floor(scrollTop / itemHeight)
- 数据的结束索引endIndex = startIndex + visibleCount
- 列表显示数据为visibleData = listData.slice(startIndex,endIndex)
当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。
- 偏移量startOffset = scrollTop - (scrollTop % itemHeight);
最终的简易代码如下:
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemHeight + 'px',lineHeight: itemHeight + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>
export default {
name:'VirtualList',
props: {
//所有列表数据
listData:{
type:Array,
default:()=>[]
},
//每项高度
itemHeight: {
type: Number,
default:200
}
},
computed:{
//列表总高度
listHeight(){
return this.listData.length * this.itemHeight;
},
//可显示的列表项数
visibleCount(){
return Math.ceil(this.screenHeight / this.itemHeight)
},
//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
data() {
return {
//可视区域高度
screenHeight:0,
//偏移量
startOffset:0,
//起始索引
start:0,
//结束索引
end:null,
};
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemHeight);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemHeight);
}
}
};