如何实现表格虚拟数据加载

最近我用的 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>

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是使用 jQuery 实现虚拟列表展示的示例代码: HTML: ```html <div class="table-container"> <table> <thead> <tr> <th>ID</th> <th>Name</th> <th>Age</th> </tr> </thead> <tbody></tbody> </table> </div> ``` CSS: ```css .table-container { height: 300px; overflow: auto; } table { width: 100%; border-collapse: collapse; } th, td { padding: 8px; border: 1px solid #ccc; } ``` JavaScript: ```javascript $(function() { // 模拟数据 var data = []; for (var i = 1; i <= 100000; i++) { data.push({ id: i, name: 'User ' + i, age: Math.floor(Math.random() * 50) + 20 }); } // 每页显示的行数 var pageSize = 50; // 当前页码 var currentPage = 1; // 当前页面显示的数据 var currentData = []; // 渲染表格 function renderTable() { var $tbody = $('tbody'); $tbody.empty(); for (var i = 0; i < currentData.length; i++) { var item = currentData[i]; var $tr = $('<tr>'); $('<td>').text(item.id).appendTo($tr); $('<td>').text(item.name).appendTo($tr); $('<td>').text(item.age).appendTo($tr); $tbody.append($tr); } } // 更新数据 function updateData() { var start = (currentPage - 1) * pageSize; var end = currentPage * pageSize; currentData = data.slice(start, end); renderTable(); } // 初始化数据 updateData(); // 监听滚动事件 $('.table-container').on('scroll', function() { var scrollTop = $(this).scrollTop(); var containerHeight = $(this).height(); var tableHeight = $('table').height(); if (scrollTop + containerHeight >= tableHeight) { currentPage++; updateData(); } }); }); ``` 在这个示例中,我们模拟了一个包含 10 万条数据表格,每次只渲染当前页的数据。当用户滚动到页面底部时,自动下一页数据。这样可以大大减少需要处理和渲染的元素数量,从而提高页面性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值