element中的Table改造成虚拟列表,并封装成hooks

前言

公司一款产品上线之后,发现表格显示了几千条数据很卡顿,而且用的都是element-plus的table,并且表格是可以编辑。每个单元格会有不同的编辑组件,因此如果考虑使用其他虚拟Table,那么改造起来就比较复杂,因此直接在之前的基础上进行改造;

原理

  1. 监听table中滚动元素的滚动事件;
  2. 在滚动事件中获取到滚动的高度,通过滚动的高度除以表格中第一列的高度(每行都是固定高度)就是下次要渲染列表的起始索引;
  3. 通过计算属性来根据起始索引和每页显示的条数来截取下次渲染的数据;
  4. 通过监听数据的长度变化,来设置滚动内容的高度,用来显示对应的滚动条;
  5. 注意:此文是针对定高的列表,非定高后续文章介绍,并且需要给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
 };
}
相关推荐
你的人类朋友4 分钟前
🤔什么时候用BFF架构?
前端·javascript·后端
知识分享小能手21 分钟前
Bootstrap 5学习教程,从入门到精通,Bootstrap 5 表单验证语法知识点及案例代码(34)
前端·javascript·学习·typescript·bootstrap·html·css3
一只小灿灿35 分钟前
前端计算机视觉:使用 OpenCV.js 在浏览器中实现图像处理
前端·opencv·计算机视觉
前端小趴菜051 小时前
react状态管理库 - zustand
前端·react.js·前端框架
Jerry Lau1 小时前
go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
前端·golang·gin
我命由我123452 小时前
前端开发问题:SyntaxError: “undefined“ is not valid JSON
开发语言·前端·javascript·vue.js·json·ecmascript·js
0wioiw02 小时前
Flutter基础(前端教程③-跳转)
前端·flutter
Jokerator2 小时前
深入解析JavaScript获取元素宽度的多种方式
javascript·css
落笔画忧愁e2 小时前
扣子Coze纯前端部署多Agents
前端
海天胜景2 小时前
vue3 当前页面方法暴露
前端·javascript·vue.js