虚拟长列表可应用于遇到不使用分页方式来加载长列表的需求。如在数据长度大于 1000或10000 条时,DOM 元素的创建和渲染需要的时间成本很高,完整渲染列表所需要的时间会造成很不好的用户体验,同时会存在滚动时卡顿问题
实现思路
虚拟列表的核心思想为渲染可视区域的数据列表,在页面滚动的同时对数据进行截取、复用DOM进行展示的渲染方式。
- 进入页面,DOM元素加载时(mounted钩子),计算出当前可视区(滚动区)的高度。同时还有计算出总数据所占的高度(滚动条的高度)
可视区的高度 = 每一项数据的高度 * 页面上显示的个数
总高度 = 每一项数据的高度 * 总列表.length
- 计算当前可见区域起始数据的 start 以及 当前可见区域结束数据的 end。然后在总数据列表中截取当前所需的列表数据
总数据列表.slice(从第几个数据开始, 截取到第几个数据)
- 监听滚动事件,通过计算出当前距离顶部的距离,然后计算出当前应该从 第几个开始显示、显示到第几个
第几个开始显示 = 当前滚动头部的距离 / 每一项的高度
显示到第几个 = 第几个开始显示 + 一页显示的个数
- 计算 当前显示在页面上第一个的数据 在整个列表中的偏移位置 scrollTop,并设置到列表上
可见区域起始数据的start * 每一项数据的高度
封装 ViualList.vue 组件
<template>
<!-- 滚动列表盒子 -->
<div class="vitual-con" ref="vitualConRef" @scroll="handleScroll">
<!-- 滚动条 -->
<div class="scroll-bar" ref="scrollBarRef"></div>
<!-- 内容区域 -->
<div class="scroll-list" ref="scrollListRef" :style="{ 'padding-top': `${scrollTop}px` }">
<div v-for="item in visibleData" :key="item.id" class="listItem" :data-i="item.id">
<slot name="itemList" :data="item"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
size: Number, // 每一项的高度
remain: Number, // 页面显示的个数
dataList: Array // 总列表数据
},
data() {
return {
start: 0, // 从第几个开始显示,默认从第一开始显示
end: this.remain // 显示到第几个,默认显示到第八个(一页默认显示8个),
}
},
methods: {
handleScroll() {
// 通过计算出当前 滚动列表盒子 距离顶部的距离,然后计算出当前应该从 第几个开始显示,显示到第几个
// this.start = 当前滚动头部的距离 / 每一项的高度
// this.end = this.start当前第一个显示项的索引 + this.remain一个显示的个数
// ~是js里的按位取反操作符,~~就是执行两次按位取反,其实就是保持原值,这里也可以使用 Math.floor() 向下取整
this.start = ~~(this.$refs.vitualConRef.scrollTop / this.size)
this.end = this.remain + this.start
}
},
computed: {
// 计算 当前应该显示的数据---会根据 this.start, this.end 自动去截取要显示的数据
visibleData() {
let start = this.start - this.preveCount
let end = this.end + this.nextCount
return this.dataList.slice(start, end)
},
// 计算内容区域 当前距离顶部的距离----优化
scrollTop() {
return this.start * this.size - this.size * this.preveCount
},
// 优化-----避免下方出现的空白和当用户快速滚动时,出现空白屏----解决:预留加载
preveCount() {
// 前面预留的个数---- 当前面的个数小于 8 个时,有几个就预留几个
return Math.min(this.start, this.remain)
},
nextCount() {
// 后面预先加载的个数
return Math.min(this.end, this.dataList.length - this.end)
}
},
mounted() {
// 1、页面一加载时,应该先计算出 滚动列表盒子 的高度 每一项的高度 * 页面显示的个数
this.$refs.vitualConRef.style.height = this.size * this.remain + 'px'
// 2、计算出总高度 每一项的高度 * 总列表数据的数量
this.$refs.scrollBarRef.style.height = this.size * this.dataList.length + 'px'
}
}
</script>
<style lang="less" scoped>
.vitual-con {
overflow-y: scroll;
position: relative;
}
.scroll-list {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
</style>
使用
<template>
<div id="app">
<!-- size 每一项的高度 -->
<!-- remain 页面显示的个数 -->
<!-- dataList 列表数据 -->
<VitualList :size="40" :remain="8" :dataList="dataList">
<template #itemList="scope">
<div class="item">{{ scope.data.value }}</div>
</template>
</VitualList>
</div>
</template>
<script>
import VitualList from '@/components/ViualList.vue'
/* 模拟数据 */
let dataList = []
for (let i = 0; i < 1000; i++) {
dataList.push({
id: i + 1,
value: '当前为第' + i + '项'
})
}
export default {
name: 'App',
data() {
return {
dataList: dataList
}
},
components: {
VitualList
}
}
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.item {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #ededed;
}
</style>