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
 };
}
相关推荐
人工智能训练19 分钟前
【极速部署】Ubuntu24.04+CUDA13.0 玩转 VLLM 0.15.0:预编译 Wheel 包 GPU 版安装全攻略
运维·前端·人工智能·python·ai编程·cuda·vllm
会跑的葫芦怪1 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js
xiaoqi9222 小时前
React Native鸿蒙跨平台如何进行狗狗领养中心,实现基于唯一标识的事件透传方式是移动端列表开发的通用规范
javascript·react native·react.js·ecmascript·harmonyos
jin1233222 小时前
React Native鸿蒙跨平台剧本杀组队消息与快捷入口组件,包含消息列表展示、快捷入口管理、快捷操作触发和消息详情预览四大核心功能
javascript·react native·react.js·ecmascript·harmonyos
烬头88214 小时前
React Native鸿蒙跨平台实现二维码联系人APP(QRCodeContactApp)
javascript·react native·react.js·ecmascript·harmonyos
pas1364 小时前
40-mini-vue 实现三种联合类型
前端·javascript·vue.js
摇滚侠4 小时前
2 小时快速入门 ES6 基础视频教程
前端·ecmascript·es6
2601_949833394 小时前
flutter_for_openharmony口腔护理app实战+预约管理实现
android·javascript·flutter
珑墨4 小时前
【Turbo】使用介绍
前端
军军君015 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three