最近我用的 table 数据越来越多了,一旦导入数据就贼卡,发愁的过程去找了下虚拟加载的表格,因为都是组件,没法解决我其他需要虚拟加载的展示需求,网上例子不多,索性自己写一个吧
不依赖其他的库,只可以满足基本需求,有需要的拿去改吧
interface Options {
itemHeight: number;
startIndex: number;
renderLen: number;
}
export const createFeedData = <T>(
scrollEle: HTMLElement & { oldScrollTop?: number },
datas: T[],
callback: (datas: T[], index: number) => any,
options?: Partial<Options>
) => {
if (scrollEle.children.length !== 1) {
const error = new Error(`
你的模型需要是:
<div>
<[ul]>
<[li]>数据</[li]>
<[li]>数据</[li]>
<[li]>数据</[li]>
</[ul]>
</div>
`);
console.error(error);
throw error;
}
const { itemHeight, renderLen, startIndex } = Object.assign(
{ itemHeight: 60, renderLen: 20, startIndex: 0 },
options
);
if (renderLen % 4 !== 0) {
const error = new Error("renderLen 必须是 4 的倍数");
console.error(error);
throw error;
}
const dataLen = datas.length;
const stepLen = renderLen / 4;
const stepHeight = stepLen * itemHeight;
const maxIndex = dataLen - renderLen;
let currentIndex = startIndex;
if (scrollEle.getBoundingClientRect().height > stepHeight * 2) {
const error = new Error("滚动盒子的高度必须 < renderLen * itemHeight / 2");
console.error(error);
throw error;
}
const redatas = () => callback(datas.slice(currentIndex, currentIndex + renderLen), currentIndex);
const reindex = (direction: "ttb" | "btt", count: number) => {
if (direction === "ttb") {
count = Math.min(count, (maxIndex - currentIndex) / stepLen);
if (count === 0 || currentIndex >= maxIndex) return;
const h = count * stepHeight;
topRef.addHeight = h;
bottomRef.delHeight = h;
currentIndex += count * stepLen;
}
if (direction === "btt") {
count = Math.min(count, currentIndex / stepLen);
if (count === 0 || currentIndex === 0) return;
const h = count * stepHeight;
topRef.delHeight = h;
bottomRef.addHeight = h;
currentIndex -= count * stepLen;
}
redatas();
};
const createEleRef = (el: HTMLElement) => {
return {
get height() {
return el.getBoundingClientRect().height;
},
set height(v: number) {
el.style.height = `${v}px`;
},
set addHeight(v: number) {
el.style.height = `${el.getBoundingClientRect().height + v}px`;
},
set delHeight(v: number) {
el.style.height = `${el.getBoundingClientRect().height - v}px`;
},
get rect() {
return el.getBoundingClientRect();
},
};
};
const dataEle = scrollEle.firstElementChild as HTMLElement;
const dataRef = createEleRef(dataEle);
const topEle = document.createElement("div");
const topRef = createEleRef(topEle);
topRef.height = startIndex * itemHeight;
scrollEle.insertBefore(topEle, dataEle);
const bottomEle = document.createElement("div");
const bottomRef = createEleRef(bottomEle);
bottomRef.height = (dataLen - startIndex - renderLen) * itemHeight;
scrollEle.appendChild(bottomEle);
let frameRequest: number;
const scrollRef = createEleRef(scrollEle);
const oldAnchor = scrollEle.style.overflowAnchor;
scrollEle.style.overflowAnchor = "none";
scrollEle.scrollTop = Math.min(
Math.max(topRef.height, scrollEle.oldScrollTop ?? 0),
topRef.height + renderLen * itemHeight - scrollRef.height
);
const scrollEvent = () => {
window.cancelAnimationFrame(frameRequest);
frameRequest = window.requestAnimationFrame(() => {
const ttb = scrollRef.rect.bottom - (dataRef.rect.bottom - stepHeight);
if (ttb > 0) return reindex("ttb", Math.ceil(ttb / stepHeight));
const btt = dataRef.rect.top + stepHeight - scrollRef.rect.top;
if (btt > 0) return reindex("btt", Math.ceil(btt / stepHeight));
});
};
scrollEle.addEventListener("scroll", scrollEvent);
redatas();
return () => {
scrollEle.oldScrollTop = scrollEle.scrollTop;
scrollEle.style.overflowAnchor = oldAnchor;
scrollEle.removeChild(topEle);
scrollEle.removeChild(bottomEle);
scrollEle.removeEventListener("scroll", scrollEvent);
};
};
我习惯用 vue 开发,所以写一个 vue 的实现
简单测了一下,看上去 10万条数据也不会卡
<script setup lang="ts">
import { createFeedData } from "~/assets/scripts/feedData";
const scrollEle = ref<HTMLDivElement>();
const lis = Array(100)
.fill(0)
.map((_, index) => index);
const renderLis = ref<number[]>([]);
onMounted(() => {
if (!scrollEle.value) return;
const remove = createFeedData(
scrollEle.value,
lis,
(datas) => {
renderLis.value = datas;
},
{ startIndex: 20 }
);
onUnmounted(() => remove());
});
const cols = [
{ prop: "name", title: "姓名" },
{ prop: "age", title: "年龄" },
{ prop: "sex", title: "性别" },
];
const scrollEle2 = ref<HTMLDivElement>();
const datas = shallowRef<Record<string, any>[]>([]);
const renderLen = 20;
const cellHeight = 60;
const vindex = ref(0);
const vdatas = ref<any[]>([]);
onMounted(() => {
const tableBox = scrollEle2.value;
if (!tableBox) return;
watch(
datas,
(datas, _, cleanup) => {
const remove = createFeedData(
tableBox,
datas,
(vDatas, index) => {
vdatas.value = vDatas;
vindex.value = index;
},
{
renderLen,
itemHeight: cellHeight,
startIndex: vindex.value,
}
);
cleanup(remove);
},
{ immediate: true }
);
});
const onClickAdd = () => {
const len = datas.value.length;
for (let i = len; i < len + 100; i++) {
datas.value.push({ name: `Name${i}`, age: i, sex: i % 2 === 0 ? "男" : "女" });
}
datas.value = [...datas.value];
};
onClickAdd();
</script>
<template>
<div>
<div ref="scrollEle" class="w-400px h-300px overflow-y-auto border-1 border-solid">
<ul>
<li class="h-60px even:bg-red odd:bg-green" v-for="i in renderLis">{{ i }}</li>
</ul>
</div>
<button @click="onClickAdd">add datas</button>
<div ref="scrollEle2" class="h-300px overflow-y-auto relative">
<table class="w-1/2">
<thead class="sticky top-0 bg-white">
<tr>
<th v-for="col in cols">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr class="even:bg-gray" v-for="data in vdatas">
<td v-for="col in cols" class="text-center" :style="{ height: `${cellHeight}px` }">{{ data[col.prop] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>