最近项目遇到一个问题,如何加载大量的数据,试了一下,加载几千条dom过多会导致页面太卡,看了网上的解决方案,现在解决多数据渲染,相信大家可能会想到分页,触底加载,懒加载等等,但其实虚拟列表也是多数据高性能加载的一个重要解决方案。
我这边理解也不是很深,讲一下大致的实现思路
首先,理解虚拟列表的原理,一次性加载大量数据,会导致页面dom太多,所以我们每次只加载对应的视窗里面的dom,其他地方的数据不加载,但是为了渲染高度,所以用padding代替,用100条数据理解,视窗高度1000px,一条列表100px,则我们就加载10条数据,剩下的90用padding代替,则设置dom padding-top为0,padding-bottom为 (100 - 10)* 100px,滚动之后,在改变对应的值
思路之后就是实现
1.获取可是区域高度
// 获取可视区域的高度
this.boxHeight = Math.max(document.body.scrollHeight,document.documentElement.scrollHeight);
2.计算每页显示行数
pageNum() {
return Math.ceil(this.boxHeight / this.itemHeight);
},
3.滚动之后计算第一条数据的index,ref为外层dom,if条件是为了滚动太快之后,target计算不准确,可以自己试试就知道了
startScroll() {
let target = Math.floor(this.$refs.scrollBox.scrollTop / this.itemHeight);
if(target > this.allHX.length - this.pageNum){
this.startNum = this.allHX.length - this.pageNum;
return
}
this.startNum = target;
},
4.视窗渲染为(start,end),所以需获取结束index
endNum() {
return this.startNum + this.pageNum;
},
5.根据上面的数据获取视窗渲染数据,为了不影响原数组,采用slice
showHX() {
return this.allHX.slice(this.startNum, this.endNum);
}
到这差不多就结束了,然后就是渲染了。
虚拟列表原理我的理解是,每次滚动的时候(滚动距离不超过1条数据高度的时候),数据是不会变化的,此时假设展示10条,则显示0-9,然后滚动超过了1条数据,此时start和end改变,会渲染1-10的数据,但是由于滚动,第一条数据在外面去了,然后padding发生变化,在把它挤到视窗开始的位置。顺序不分先后,不清楚~~
写了个demo
gitee地址:vue虚拟列表: vue虚拟列表
完整代码如下
<template>
<div class="scroll-main" @scroll="fn" ref="scrollBox">
<div :style="{paddingTop:startNum * itemHeight + 'px',paddingBottom:(allHX.length - endNum) * itemHeight + 'px'}">
<div v-for="(item, index) in showHX" :key="item.id" class="item" :style="{height: (itemHeight - 2) + 'px'}">
<span>{{item.id}}-{{item.msg}}</span>
<span @click="deleteItem(index)">删除</span>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
allHX: [],
startNum: 0,
boxHeight: 0,
fn: this.debounce(this.startScroll, 100),
wishList:['洗脚', '按摩', '给零花钱', '做饭', '买车', '买房', '做项目', '带娃', '应付丈母娘']
};
},
props: {
itemHeight: {
type: String,
default: '100'
}
},
created() {
this.getMock(100000);
},
mounted() {
this.getBoxHeight();
},
methods: {
getMock(count) {
for (let i = 0; i < count; i++) {
this.allHX.push({
id: `HX${i}号`,
msg: `负责给kfq${this.wishList[i%9]}`,
});
}
},
getBoxHeight() {
// 获取可视区域的高度
this.boxHeight = Math.max(document.body.scrollHeight,document.documentElement.scrollHeight);
},
startScroll() {
let target = Math.floor(this.$refs.scrollBox.scrollTop / this.itemHeight);
if(target > this.allHX.length - this.pageNum){
this.startNum = this.allHX.length - this.pageNum;
return
}
this.startNum = target;
},
deleteItem(index) {
this.allHX.splice(this.startNum + index, 1);
},
debounce(fun, wait) {
let timer = null;
return function (){
if(timer !== null) clearTimeout(timer);
timer = setTimeout(fun, wait);
}
}
},
computed: {
pageNum() {
return Math.ceil(this.boxHeight / this.itemHeight);
},
endNum() {
return this.startNum + this.pageNum;
},
showHX() {
return this.allHX.slice(this.startNum, this.endNum);
}
}
};
</script>
<style scoped>
.scroll-main{
height: 100%;
overflow: scroll;
overflow-x: hidden;
}
.item{
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid red;
padding: 0 20px;
}
</style>