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

相关推荐
IT_陈寒3 小时前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
guslegend4 小时前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
大包子5 小时前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
海码事务所5 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
田威AI5 小时前
图片内文字翻译的规格:输入输出、保真、自动化、时间与费用
前端·计算机视觉
不可能片场5 小时前
命令行中文变问号 我用环境变量救了场
前端·electron
lerhxx5 小时前
AI 应用中的上下文管理 —— 从"上下文窗口"到"分层压缩"的工程实践
前端·javascript
骑着蜗牛撵大象3275 小时前
多 Agent 分治协作:MapReduce 模式下的并行扇出与结果归并
前端
用户55318297325 小时前
Flutter iOS 热更新深入 Dart VM:读懂函数入口、解释执行与调用桥接
前端
前端snow5 小时前
为什么大厂要用Postgres + mogodb的框架?
前端