-
背景:当后端返回的数据无法分页,且数据量大。前端需要使用虚拟列表技术,减少大量dom节点的渲染。
-
核心思想:通过计算可视区域范围,动态渲染可见的列表项,避免渲染全部数据。
-
关键参数:
containerHeight:容器高度(可视区域)itemHeight:每个列表项的高度(固定或动态)startIndex:起始渲染索引endIndex:结束渲染索引scrollTop:滚动条位置
VirtualList.vue
<template>
<div
class="virtual-list-container"
ref="containerRef"
@scroll="handleScroll"
>
<!-- 占位元素,撑开滚动条高度 -->
<div
class="phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<!-- 实际渲染的列表项 -->
<div
class="list"
:style="{ transform: `translateY(${offset}px)` }"
>
<div
v-for="item in visibleData"
:key="item.id"
class="list-item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.content }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const props = defineProps({
data: { type: Array, required: true }, // 所有数据
itemHeight: { type: Number, default: 50 }, // 每项高度(固定高度)
bufferSize: { type: Number, default: 5 } // 缓冲区大小(额外渲染的项数)
});
const containerRef = ref(null);
const scrollTop = ref(0);
// 计算总高度(撑开滚动条)
const totalHeight = computed(() => props.data.length * props.itemHeight);
// 计算可见区域的起始/结束索引
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.bufferSize);
});
const endIndex = computed(() => {
return Math.min(
props.data.length - 1,
Math.floor((scrollTop.value + containerHeight.value) / props.itemHeight) + props.bufferSize
);
});
// 容器高度(通过DOM获取)
const containerHeight = ref(0);
onMounted(() => {
containerHeight.value = containerRef.value?.clientHeight || 0;
});
// 计算偏移量(保证列表项在可视区域内)
const offset = computed(() => {
return Math.max(0, startIndex.value * props.itemHeight);
});
// 当前可见的数据切片
const visibleData = computed(() => {
return props.data.slice(startIndex.value, endIndex.value + 1);
});
// 滚动事件处理
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop;
};
</script>
<style scoped>
.virtual-list-container {
height: 500px; /* 容器固定高度 */
overflow-y: auto; /* 启用垂直滚动 */
position: relative; /* 相对定位 */
border: 1px solid #eee;
}
.phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1; /* 置于底层 */
}
.list {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.list-item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #f0f0f0;
}
</style>
使用示例
<template>
<VirtualList :data="listData" :itemHeight="60" />
</template>
<script setup>
import { ref } from 'vue';
import VirtualList from './VirtualList.vue';
// 生成测试数据(30万条)
const listData = ref(
Array.from({ length: 300000 }, (_, i) => ({
id: i,
content: `列表项 ${i + 1}`
}))
);
</script>