vue 虚拟列表
虚拟列表
什么是虚拟列表
虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术
为什么需要虚拟列表
虚拟列表是对长列表的一种优化方案。在前端开发中,会碰到一些不能使用分页方式来加载列表数据的业务形态,我们称这种列表叫做长列表。比如,手机端,淘宝商品展示,美团外卖等,数据量特别庞大,不适合分页,以及懒加载,这时候我们可以采用虚拟列表,只展示可视区域数据。
直接上代码
HTML
<div>
<div class="list-view" ref="container" @scroll="handleScroll">
<div class="list-view-phantom" ref="clientHeight" :style="{ height: contentHeight + 'px' }"></div>
<ul ref="content" class="list-view-content">
<li class="list-view-item" :style="{ height: itemHeight + 'px' }" :key="index" v-for="(val, index) in list">
{{ val }}
</li>
</ul>
</div>
</div>
js
export default {
name: 'ListView',
computed: {
contentHeight() {
// 计算滚动条高度
return this.data.length * this.itemHeight;
}
},
mounted() {
this.getData();
this.update();
},
data() {
return {
data: [], // 总数据
itemHeight: 30, // 单个高度
list: [], // 渲染数据
};
},
methods: {
update(scrollTop = 0) {
// 获取当前可展示数量
const count = Math.ceil(this.$el.clientHeight / this.itemHeight);
// 取得可见区域的起始数据索引
const start = Math.floor(scrollTop / this.itemHeight);
// 取得可见区域的结束数据索引
const end = start + count;
// 计算出可见区域对应的数据,让 Vue.js 更新
this.list = this.data.slice(start, end);
// 把可见区域的 top 设置为起始元素在整个列表中的位置(使用 transform 是为了更好的性能)
this.$refs.content.style.webkitTransform = `translate3d(0, ${ start * this.itemHeight }px, 0)`;
},
handleScroll(e) {
// 获取当前滚动条滚动位置
const scrollTop = this.$refs.container.scrollTop;
this.update(scrollTop);
},
getData() {
//创建模拟数据
let data = [];
for (let i = 0; i < 1000000; i++) {
data.push(`第 ${i} 个数据`)
}
this.data = [...data];
}
}
}
CSS
.list-view {
width: 400px;
height: 400px;
overflow: auto;
position: relative;
border: 1px solid #c1c1c1;
box-shadow: 3px 3px 5px #ccc;
}
.list-view-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: relative;
}
.list-view-item {
padding: 6px;
color: #999;
line-height: 30px;
}
vue3 写法
<template>
<div>
<div class="list-view" ref="container" @scroll="handleScroll">
<!-- 占位的虚拟高度 -->
<div class="list-view-phantom" :style="{ height: contentHeight + 'px' }"></div>
<ul ref="content" class="list-view-content">
<!-- 渲染可见区域的列表项 -->
<li
class="list-view-item"
:style="{ height: itemHeight + 'px' }"
v-for="(val, index) in list"
:key="index"
>
{{ val }}
</li>
</ul>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
export default {
name: 'VirtualScrollList',
setup() {
// 数据和配置
const data = ref([]); // 总数据
const list = ref([]); // 当前渲染的数据
const itemHeight = 30; // 每个列表项的高度
const container = ref(null); // 容器的 DOM 引用
const content = ref(null); // 内容区的 DOM 引用
// 计算虚拟滚动总高度
const contentHeight = computed(() => data.value.length * itemHeight);
// 更新可见数据
const update = (scrollTop = 0) => {
if (!container.value) return;
// 当前容器可展示的元素数量
const visibleCount = Math.ceil(container.value.clientHeight / itemHeight);
// 起始数据索引
const startIndex = Math.floor(scrollTop / itemHeight);
// 结束数据索引
const endIndex = startIndex + visibleCount;
// 提取当前可见数据
list.value = data.value.slice(startIndex, endIndex);
// 移动可见区域内容到正确的起始位置
if (content.value) {
content.value.style.transform = `translate3d(0, ${startIndex * itemHeight}px, 0)`;
}
};
// 滚动事件处理
const handleScroll = () => {
const scrollTop = container.value.scrollTop;
update(scrollTop);
};
// 模拟获取数据
const getData = () => {
const mockData = Array.from({ length: 1000000 }, (_, i) => `第 ${i} 个数据`);
data.value = mockData;
update(); // 初次更新
};
// 挂载时加载数据并初始化
onMounted(() => {
getData();
});
return {
container,
content,
contentHeight,
list,
handleScroll,
itemHeight,
};
},
};
</script>
<style scoped lang="less">
.list-view {
width: 400px;
height: 400px;
overflow: auto;
position: relative;
border: 1px solid #c1c1c1;
box-shadow: 3px 3px 5px #ccc;
}
.list-view-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: relative;
}
.list-view-item {
padding: 6px;
color: #999;
line-height: 30px;
}
</style>