前言
在前端开发领域,表格一直都是一个高频出现的组件,尤其是在中后台和数据分析场景。 但是,对于 Table V1来说,当一屏里超过 1000 条数据记录时,就会出现卡顿等性能问题,体验不是很好。
通过虚拟化表格组件,超大数据渲染将不再是一个头疼的问题。
方案
- 使用elememt plus提供的组件 Virtualized Table 虚拟化表格
- 使用自定义虚拟化组件
原因
- 获取全部数据属于后期新增需求
- 基于设计模式开放封闭原则
- 在原有已开发的基础上添加代码相比于重新开发相对方便
- 数据格式统一
- UI样式无需更改
附源码
<template>
<div ref="onlyVirtual" class="only-virtual">
<el-table
ref="onlyTable"
:data="dataList"
:row-key="onlyKey"
:highlight-current-row="highlightCurrentRows"
height="100%"
:fit="fit"
v-loading="onlyLoading"
>
<slot :start="start"></slot>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed, watch, onUnmounted, onBeforeUnmount } from 'vue';
const props = defineProps({
rowKey: String, // 行数据的key0
// 是否高亮当前行
highlightCurrentRow: {
type: Boolean,
default: false
},
// 可视区高度
showHeight: {
type: Number,
default: 0
},
// 单行高度
itemHeight: {
type: Number,
default: 52
},
// 列表数据
tableList: {
type: Array,
default: []
},
// 当数据量超过一定数值时使用虚拟列表
threshold: {
type: Number,
default: 500
},
// 列的宽度是否自撑开
fit: {
type: Boolean,
default: true
},
onlyLoading: {
type: Boolean,
default: false
}
});
// 行数据的key
const onlyKey = computed(() => props.rowKey) || '';
// 是否高亮当前行
const highlightCurrentRows = computed(() => props.highlightCurrentRow);
// 外层dom节点
const onlyVirtual = ref();
// 表格的引用实例
const onlyTable = ref();
// 可视区数据起始索引
const start = ref(0);
// 可视区数据结束索引
const end = ref(0);
const fit = computed(() => props.fit);
const onlyLoading = computed(() => props.onlyLoading);
// 总数据长度
const allListLength = computed(() => props.tableList.length);
// 总数据高度
const maxHeight = computed(() => props.tableList.length * props.itemHeight);
// 可显示数据条数 = 向上取整(可视区高度 / 单行高度)
const pageSize = computed(() => Math.ceil(props.showHeight / props.itemHeight));
// 可视区列表数据
const dataList = computed(() => {
// 当数据量超过限定值时使用虚拟列表
if (allListLength.value > props.threshold) {
return props.tableList.slice(start.value, end.value);
}
return props.tableList;
});
// 监听数据长度
watch(allListLength, (val) => {
// 获取插入的元素
const scrollHeight: any = document.getElementById('onlyHeight');
if (val > props.threshold) {
// 设置总高度,用来显示滚动条
scrollHeight.style.height = maxHeight.value + 'px';
// 注册自定义滚动事件
initScroll();
} else {
scrollHeight.style.height = '';
// 移除自定义滚动事件
removeScroll();
}
});
// 监听可视区高度
watch(
() => props.showHeight,
(val) => {
// 固定可视区高度
onlyVirtual.value.style.height = val + 'px';
// 显示区数据结束索引
end.value = start.value + pageSize.value;
if (val && allListLength.value > props.threshold) {
let _bodyWrapper = onlyTable.value.$refs.bodyWrapper;
_bodyWrapper.style.height = val + 'px';
}
}
);
// 插入空元素用于显示滚动条
const addElement = () => {
let i = document.createElement('div');
i.id = 'onlyHeight';
i.style.width = '1px';
i.style.float = 'right';
let _wrapRef = onlyTable.value?.$refs?.scrollBarRef?.wrapRef;
_wrapRef?.append(i);
};
// 滚动事件
const scrollListener = (event: any) => {
let _bodyWrapper = onlyTable.value?.$refs?.bodyWrapper;
let _bodyWrapper_body = onlyTable.value?.$refs?.tableBody;
// 滚动过的距离
let _scrollTop = event.target.scrollTop;
if (_scrollTop >= maxHeight.value - _bodyWrapper?.offsetHeight) {
_scrollTop = maxHeight.value - _bodyWrapper?.offsetHeight;
}
// 起始数据索引/向下取整
start.value = Math.floor(_scrollTop / props.itemHeight);
// 结束数据索引
end.value = start.value + pageSize.value;
// 将整个表格往下移
_bodyWrapper_body.style.transform = `translateY(${_scrollTop}px)`;
};
// 注册自定义滚动事件
const initScroll = () => {
let _wrapRef = onlyTable.value?.$refs?.scrollBarRef?.wrapRef;
_wrapRef.scrollTop = 0; // 回到顶部
_wrapRef?.addEventListener('scroll', scrollListener);
};
// 移除自定义滚动事件
const removeScroll = () => {
let _wrapRef: any = onlyTable.value?.$refs?.scrollBarRef?.wrapRef;
_wrapRef?.removeEventListener('scroll', scrollListener);
// 样式移除
let _bodyWrapper_body: any = onlyTable.value?.$refs?.tableBody;
_bodyWrapper_body.style.transform = '';
// 重置起始值
start.value = 0;
};
onMounted(() => {
addElement();
});
onBeforeUnmount(() => {
removeScroll();
})
</script>
<style lang="scss" scoped>
.only-virtual {
overflow-y: hidden;
}
:deep(.el-scrollbar__wrap--hidden-default) {
display: flex;
}
</style>
使用方式
<VirtualTable
rowKey="uId"
:tableList="listFilter"
:highlightCurrentRow="!isSkip"
:showHeight="showHeight"
:itemHeight="itemHeight"
:threshold="500"
#default="{start}"
>
<el-table-column type="index" width="60" label="序号">
<template #default="scope">
<span>{{ scope.$index + 1 + start }}</span>
</template>
</el-table-column>
<el-table-column label="姓名">
<template #default="scope">
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
...
</VirtualTable>
偶尔记录一下...