web技术分享| 虚拟列表实现
针对过多数据列表展示造成过多节点渲染使页面卡死或卡顿,特地封装一个简易的虚拟列表,大家可在此基础上进行针对修改
组件基于 vue3 + element plus + ts + tailwindcss 开发
设计思路
分为三部分:
- 父容器占位;
- 一个子容器展示通过滚动条计算的数据相当于真实渲染节点;
- 一个子容器作为虚拟列表不渲染列表提供列表真实高度显示滚动条
实现
页面结构
<el-scrollbar
class="scrollbar_custom px-4"
:height="virtualRecord.height"
@scroll="handleScroll"
>
<!-- 虚拟高度 -->
<div
class="-z-10 absolute inset-0"
:style="{ height: virtualRecord.virtualHeight + 'px' }"
></div>
<!-- 真实列表 -->
<ul
:class="['absolute inset-0', prop.customClass]"
:style="{ transform: `translateY(${virtualRecord.offset}px)` }"
>
<li
v-for="(item, index) in virtualRecord.visibleData"
:key="index + '_'"
:style="{
height: virtualRecord.itemHeight + 'px',
}"
class="hover:bg-anyrtc-gray_8 text-sm"
>
<!-- 插槽 -->
<slot :item="item"></slot>
</li>
</ul>
</el-scrollbar>
样式
修改 element plus 的 scrollbar 样式
.scrollbar_custom {
:deep(.el-scrollbar__wrap) {
.el-scrollbar__view {
@apply relative;
}
}
逻辑
组件所需参数
const prop = defineProps({
// 自定义类名
customClass: String,
// 相关配置
option: {
type: Object,
default: () => {
return {};
},
},
listData: {
type: Array,
default: () => {
return [];
},
},
});
组件内部定义
// 组件记录(默认)
const virtualRecord = reactive({
height: 400,
// 展示几个
visibleCount: 16,
// // 刷新频率
timeout: 4,
// // 行高
itemHeight: 40,
// translateY偏移量
offset: 0,
// 虚拟占位高度
virtualHeight: 300,
// 记录滚动高度
recordScrollTop: 0,
dataList: [] as any[],
// 可展示的数据
visibleData: [] as any[],
});
// 合并配置
const mergeFn = () => {
virtualRecord.height = prop.option.height || 400;
virtualRecord.dataList = JSON.parse(JSON.stringify(prop.listData));
virtualRecord.itemHeight = prop.option.itemHeight || 40;
virtualRecord.timeout = prop.option.timeout || 4;
// // 虚拟高度
virtualRecord.virtualHeight = prop.listData.length * virtualRecord.itemHeight;
// 展示数量
virtualRecord.visibleCount = Math.ceil(
virtualRecord.height / virtualRecord.itemHeight
);
};
滚动计算
let lastTime = 0;
const handleScroll = (scroll: { scrollTop: number }) => {
let currentTime = +new Date();
if (currentTime - lastTime > virtualRecord.timeout) {
virtualRecord.recordScrollTop = scroll.scrollTop;
updateVisibleData(scroll.scrollTop);
lastTime = currentTime;
}
};
const updateVisibleData = (scrollTop: number) => {
let start =
Math.floor(scrollTop / virtualRecord.itemHeight) -
Math.floor(virtualRecord.visibleCount / 2);
start = start < 0 ? 0 : start;
const end = start + virtualRecord.visibleCount * 2;
virtualRecord.visibleData = virtualRecord.dataList.slice(start, end);
virtualRecord.offset = start * virtualRecord.itemHeight;
};
列表信息变更
watch(
() => {
return [prop.listData, prop.option];
},
([newData, newHeight]) => {
if (newData) {
// 合并数据
mergeFn();
// 更新视图
updateVisibleData(virtualRecord.recordScrollTop);
}
},
{
immediate: true,
}
);
使用
<VirtualItem :option={height:'占位高度'} :list-data="列表" >
<template #default="{ item }">
{{ item }}
</template>
</VirtualItem>