基于element-plus table组件基础上处理的虚拟列表(虚拟表格)组件

930 阅读2分钟

前言

在前端开发领域,表格一直都是一个高频出现的组件,尤其是在中后台和数据分析场景。 但是,对于 Table V1来说,当一屏里超过 1000 条数据记录时,就会出现卡顿等性能问题,体验不是很好。

通过虚拟化表格组件,超大数据渲染将不再是一个头疼的问题。

方案

  1. 使用elememt plus提供的组件 Virtualized Table 虚拟化表格
  2. 使用自定义虚拟化组件

原因

  • 获取全部数据属于后期新增需求
  • 基于设计模式开放封闭原则
  • 在原有已开发的基础上添加代码相比于重新开发相对方便
  • 数据格式统一
  • 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>

偶尔记录一下...