前言
公司一款产品上线之后,发现表格显示了几千条数据很卡顿,而且用的都是element-plus的table,并且表格是可以编辑。每个单元格会有不同的编辑组件,因此如果考虑使用其他虚拟Table,那么改造起来就比较复杂,因此直接在之前的基础上进行改造;
原理
- 监听table中滚动元素的滚动事件;
- 在滚动事件中获取到滚动的高度,通过滚动的高度除以表格中第一列的高度(每行都是固定高度)就是下次要渲染列表的起始索引;
- 通过计算属性来根据起始索引和每页显示的条数来截取下次渲染的数据;
- 通过监听数据的长度变化,来设置滚动内容的高度,用来显示对应的滚动条;
- 注意:此文是针对定高的列表,非定高后续文章介绍,并且需要给Table设置固定高度;

详解
监听table中滚动元素的滚动事件
js
const setScroll = () => {
// 获取到滚动元素进行监听
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.addEventListener("scroll", handleScroll);
}
};
在滚动事件中获取到滚动的高度,通过滚动的高度除以表格中第一列的高度(每行都是固定高度)就是下次要渲染列表的起始索引;
首先定义一些变量
js
// 获取Table的ref
const multipleTableRef = ref();
let ITEM_HEIGHT = 100; // 假设每个 item 的高度为 100px
const RENDER_SIZE = 15; // 假设每次渲染 15 条数据
const startIndex = ref(0); // 截取列表的起始索引
js
// 滚动事件
const handleScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
// 获取到滚动元素的高度
const scrollTop = tableElement?.scrollTop;
// 计算startIndex,滚动的高度/行高
startIndex.value = Math.floor(scrollTop / ITEM_HEIGHT);
// 获取到内容的元素
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement1) {
// 设置内容的paddingTop为滚动的高度
tableElement1.style.paddingTop = scrollTop + "px";
}
};
有了起始索引怎么在滚动的时候实时的截取数据展示呢?
通过计算属性来动态的根据起始索引和每页显示的条数来截取下次渲染的数据;
js
// 通过计算属性,来动态的根据起始索引的变化来截取要显示的列表
const renderItems = computed(() => {
// 起始索引+每页的数量就是这一页结束的索引
const endIndex = startIndex.value + RENDER_SIZE;
// 从数据中截取
const arr = productList.value.slice(startIndex.value, endIndex);
// 给每条数据添加一个序号,根据业务需求需要显示
arr.forEach((item, index) => {
item.num = startIndex.value + index + 1;
});
return arr;
});
怎么让Table表格按照实际的数据量显示滚动条呢?
通过监听数据的长度变化,在页面渲染之后获取到第一行的高度,通过数据的长度*这个高度就计算出整个列表的高度;
js
// 监听数据长度的变化
watch(
() => productList.value.length,
() => {
setScrollHeight();
}
);
// 获取到一项的高度,计算出整个列表的高度,赋值给滚动元素的内部元素,显示出滚动条
const setScrollHeight = () => {
// 确保table渲染完成
nextTick(() => {
// 获取到包裹列表的元素
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement) {
// 确保table中的数据也渲染完成
nextTick(() => {
// 获取第一行列表的高度
const row = tableElement.querySelector(".el-table__row");
const h = row?.getBoundingClientRect().height;
// 如果有高度就用这个高度,否则就用默认的高度
ITEM_HEIGHT = h || ITEM_HEIGHT;
// 设置包裹列表元素的高度
tableElement.style.height =
Math.ceil(productList.value.length * ITEM_HEIGHT) + Math.ceil(ITEM_HEIGHT) + "px";
});
}
});
};
优化
以上就完成了虚拟列表的核心,但是多个页签之间的table滚动会出现共享的问题,比如在a页面下滚动了table到500的高度,切换到b页面下发现table会显示空白,并且也滚动到了500的高度,因此需要在失活的钩子中存储当前页面中table的滚动位置,在激活的时候滚动到这个位置即可;
js
let curScrollTop = 0;
onActivated(() => {
// 切换回来的时候滚动到之前的位置
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.scrollTo({
top: curScrollTop || 0
});
}
});
onDeactivated(() => {
// 切换页面的时候记录当前滚动的高度
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement1) {
curScrollTop = parseFloat(tableElement1.style.paddingTop);
}
});
完整的代码
js
// html 注意要指定ref和data还有height
<el-table ref="multipleTableRef" :data="renderItems" style="width: 100%" height="600px">
<el-table-column prop="" label="" width="100">
<template #default="scope"> {{ scope.row.num }}</template>
</el-table-column>
</el-table>
js
import { ref, computed, watch, onMounted, nextTick, onActivated, onDeactivated } from "vue";
export function useTableFixedScroll(productList, itemHeight, size) {
const multipleTableRef = ref();
let ITEM_HEIGHT = itemHeight || 100; // 假设每个 item 的高度为 100px
const RENDER_SIZE = size || 15; // 假设每次渲染 15 条数据
const startIndex = ref(0);
// 通过计算属性,来动态的根据起始索引的变化来截取要显示的列表
const renderItems = computed(() => {
const endIndex = startIndex.value + RENDER_SIZE;
const arr = productList.value.slice(startIndex.value, endIndex);
arr.forEach((item, index) => {
item.num = startIndex.value + index + 1;
});
return arr;
});
// 监听table的滚动,根据滚动的高度除以每项的高度就是下次要显示列表的起始索引,再把滚动的高度设置给包裹每项元素的paddingTop,
// 这样就能显示对应的每一项
const handleScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
const scrollTop = tableElement?.scrollTop;
// 更新 startIndex
startIndex.value = Math.floor(scrollTop / ITEM_HEIGHT);
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement1) {
tableElement1.style.paddingTop = scrollTop + "px";
}
};
const setScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.addEventListener("scroll", handleScroll);
}
};
// 获取到一项的高度,计算出整个列表的高度,赋值给滚动元素的内部元素,显示出滚动条
const setScrollHeight = () => {
nextTick(() => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement) {
nextTick(() => {
const row = tableElement.querySelector(".el-table__row");
const h = row?.getBoundingClientRect().height;
ITEM_HEIGHT = h || ITEM_HEIGHT;
tableElement.style.height =
Math.ceil(productList.value.length * ITEM_HEIGHT) + Math.ceil(ITEM_HEIGHT) + "px";
});
}
});
};
// 整个列表的高度
watch(
() => productList.value.length,
() => {
setScrollHeight();
}
);
onMounted(() => {
setScroll();
});
let curScrollTop = 0;
onActivated(() => {
// 切换回来的时候滚动到之前的位置
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.scrollTo({
top: curScrollTop || 0
});
}
});
onDeactivated(() => {
// 切换页面的时候记录当前滚动的高度
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement1) {
curScrollTop = parseFloat(tableElement1.style.paddingTop);
}
});
return {
multipleTableRef,
renderItems,
handleScroll,
setScroll,
setScrollHeight
};
}