element中的table改造成虚拟列表(不定高),并封装成hooks

前言

上一篇文章介绍了element中的table改造成虚拟列表(定高),并封装成hooks,本文再次实现不定高的虚拟列表改造;

原理

  1. 获取Table中已经渲染的每行元素的实际高度,并且通过每行数据的唯一属性保存到一个对象a中;
  2. 监听table中滚动元素的滚动事件;
  3. 滚动事件中获取到滚动元素的滚动高度,遍历整个数据列表,从缓存的对象a中通过唯一属性获取到这行数据的高度,并且把每行数据的高度进行相加,如果相加的高度超过或等于了滚动的高度,那么此数据的索引就是下次渲染列表的起始索引;
  4. 通过计算属性来根据起始索引和每页显示的条数来截取下次渲染的数据;

获取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中渲染出来的列表的高度,滚动的时候遍历数据列表,记录每条数据的高度之和,如果这个值大于等于了滚动的高度表示要渲染下一页的数据,因此此时的索引也就是下页数据的起始索引,通过计算属性来实时的截取数据进行渲染;

相关推荐
姜太小白1 小时前
【前端】CSS Grid布局介绍及示例
前端·css
风继续吹..4 小时前
后台管理系统权限管理:前端实现详解
前端·vue
yuanmenglxb20044 小时前
前端工程化包管理器:从npm基础到nvm多版本管理实战
前端·前端工程化
新手小新5 小时前
C++游戏开发(2)
开发语言·前端·c++
我不吃饼干5 小时前
【TypeScript】三分钟让 Trae、Cursor 用上你自己的 MCP
前端·typescript·trae
飞翔的佩奇6 小时前
基于SpringBoot+MyBatis+MySQL+VUE实现的经方药食两用服务平台管理系统(附源码+数据库+毕业论文+部署教程+配套软件)
数据库·vue.js·spring boot·mysql·毕业设计·mybatis·经方药食两用平台
小杨同学yx6 小时前
前端三剑客之Css---day3
前端·css
Mintopia8 小时前
🧱 用三维点亮前端宇宙:构建你自己的 Three.js 组件库
前端·javascript·three.js
故事与九8 小时前
vue3使用vue-pdf-embed实现前端PDF在线预览
前端·vue.js·pdf
Mintopia9 小时前
🚀 顶点-面碰撞检测之诗:用牛顿法追寻命运的交点
前端·javascript·计算机图形学