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

相关推荐
加减法原则41 分钟前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele1 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4531 小时前
React移动端开发项目优化
前端·react.js·前端框架
天若有情6731 小时前
React、Vue、Angular的性能优化与源码解析概述
vue.js·react.js·angular.js
你的人类朋友1 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir1 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴2 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子2 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead2 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
eggcode3 小时前
Vue+Openlayers加载OSM、加载天地图
vue.js·openlayers·webgis