前言
上一篇文章介绍了element中的table改造成虚拟列表(定高),并封装成hooks,本文再次实现不定高的虚拟列表改造;
原理
- 获取Table中已经渲染的每行元素的实际高度,并且通过每行数据的唯一属性保存到一个对象a中;
- 监听table中滚动元素的滚动事件;
- 滚动事件中获取到滚动元素的滚动高度,遍历整个数据列表,从缓存的对象a中通过唯一属性获取到这行数据的高度,并且把每行数据的高度进行相加,如果相加的高度超过或等于了滚动的高度,那么此数据的索引就是下次渲染列表的起始索引;
- 通过计算属性来根据起始索引和每页显示的条数来截取下次渲染的数据;
获取Table中每行元素的实际高度,并且通过每行数据的唯一属性保存到一个对象中
js
/**
通过Ref获取每行数据的dom元素,
*/
<el-table ref="multipleTableRef" :data="renderItems" style="width: 100%" height="600px" show-overflow-tooltip>
<el-table-column prop="" label="" width="100">
<template #default="scope">
<div :ref="(el) => renderItemsRef(el, scope.row.rowId)">{{ scope.row.num }}</div></template
>
</el-table-column>
</el-table>
// 缓存已经渲染过的每行数据的高度
const hasRenderedItemsHeight = ref({})
const renderItemsRef = (el, id) => {
if (el) {
nextTick(() => {
// 存放已渲染的 item 的高度
hasRenderedItemsHeight.value[id] = el
.closest(".el-table__cell")
?.getBoundingClientRect().height;
// 更新容器的总高度,还没有渲染的数据那么就设置一个默认高度,先显示出滚动条
const h = productList.value.reduce(
(sum, item) => sum + (hasRenderedItemsHeight.value[item[itemId]] || ITEM_HEIGHT),
0
);
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement) {
tableElement.style.height = Math.ceil(h) + "px";
}
});
}
};
监听table中滚动元素的滚动事件
js
const setScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.addEventListener("scroll", handleScroll);
}
};
滚动事件中获取到滚动元素的滚动高度,遍历整个数据列表,从缓存的对象a中通过唯一属性获取到这行数据的高度,并且把每行数据的高度进行相加,如果相加的高度超过或等于了滚动的高度,那么此数据的索引就是下次渲染列表的起始索引;
js
const handleScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
// 获取到滚动得高度
const scrollTop = tableElement?.scrollTop;
// 从第0项开始记录列表元素得高度之和
let startOffset = 0;
// 遍历完整的数据列表
for (let i = 0; i < productList.value.length; i++) {
// 从缓存中获取到对应数据的实际高度
const h = hasRenderedItemsHeight.value[productList.value[i][itemId]] || ITEM_HEIGHT;
startOffset += h;
// 如果当前元素加上之前元素得高度大于了滚动得高度,那么就要从当前元素开始截取
if (startOffset >= scrollTop) {
startIndex.value = i;
break;
}
}
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
// 容器顶部需要撑开的高度
const paddingTop =
startOffset - hasRenderedItemsHeight.value[productList.value[startIndex.value][itemId]];
if (tableElement1) {
tableElement1.style.paddingTop = paddingTop + "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;
});
完整代码
js
import { ref, computed, onMounted, nextTick, onActivated, onDeactivated } from "vue";
export function useTableNFixedScroll(productList, itemId, itemHeight, size) {
// 获取table元素
const multipleTableRef = ref();
let ITEM_HEIGHT = itemHeight || 100; // 假设每个 item 的高度为 100px
const RENDER_SIZE = size || 15; // 假设每次渲染 15 条数据
const hasRenderedItemsHeight = ref({}); // 缓存已经渲染得行得高度
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;
});
const handleScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
// 获取到滚动得高度
const scrollTop = tableElement?.scrollTop;
// 从第0项开始记录列表元素得高度之和
let startOffset = 0;
for (let i = 0; i < productList.value.length; i++) {
const h = hasRenderedItemsHeight.value[productList.value[i][itemId]] || ITEM_HEIGHT;
startOffset += h;
// 如果当前元素加上之前元素得高度大于了滚动得高度,那么就要从当前元素开始截取
if (startOffset >= scrollTop) {
startIndex.value = i;
break;
}
}
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
const paddingTop =
startOffset - hasRenderedItemsHeight.value[productList.value[startIndex.value][itemId]];
if (tableElement1) {
tableElement1.style.paddingTop = paddingTop + "px";
}
};
const setScroll = () => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.addEventListener("scroll", handleScroll);
}
};
// 每项实际渲染之后的dom元素
const renderItemsRef = (el, id) => {
if (el) {
nextTick(() => {
// 存放已渲染的 item 的高度
hasRenderedItemsHeight.value[id] = el
.closest(".el-table__cell")
?.getBoundingClientRect().height;
// 更新容器的总高度
const h = productList.value.reduce(
(sum, item) => sum + (hasRenderedItemsHeight.value[item[itemId]] || ITEM_HEIGHT),
0
);
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement) {
tableElement.style.height = Math.ceil(h) + "px";
}
});
}
};
// 滚动到指定位置
const scrollToPos = (top) => {
const tableElement = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__wrap");
if (tableElement) {
tableElement.scrollTo({
top: top || 0
});
}
};
onMounted(() => {
setScroll();
});
let curScrollTop = 0;
onActivated(() => {
// 切换回来的时候滚动到之前的位置
scrollToPos(curScrollTop);
});
onDeactivated(() => {
// 切换页面的时候记录当前滚动的高度
const tableElement1 = multipleTableRef.value?.$el?.querySelector(".el-scrollbar__view");
if (tableElement1) {
curScrollTop = parseFloat(tableElement1.style.paddingTop);
}
});
return {
multipleTableRef,
renderItems,
handleScroll,
setScroll,
renderItemsRef,
scrollToPos
};
}
js
<el-table ref="multipleTableRef" :data="renderItems" style="width: 100%" height="600px" show-overflow-tooltip>
<el-table-column prop="" label="" width="100">
<template #default="scope">
<div :ref="(el) => renderItemsRef(el, scope.row.rowId)">{{ scope.row.num }}</div></template
>
</el-table-column>
</el-table>
const { multipleTableRef, renderItems, renderItemsRef } = useTableNFixedScroll(tableData, "rowId");
总结
核心就是获取并且记录Table中渲染出来的列表的高度,滚动的时候遍历数据列表,记录每条数据的高度之和,如果这个值大于等于了滚动的高度表示要渲染下一页的数据,因此此时的索引也就是下页数据的起始索引,通过计算属性来实时的截取数据进行渲染;