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

相关推荐
mustfeng2 分钟前
VCS & Verdi 2023安装
java·服务器·前端
Mintopia22 分钟前
🌐 数据合规框架下的 WebAIGC 训练数据处理技术规范
前端·javascript·aigc
骥龙41 分钟前
2.6、Web漏洞挖掘实战(下):XSS、文件上传与逻辑漏洞深度解析
前端·xss
用户433845375691 小时前
Promise深度解析,以及简易版的手写实现
前端
梦之云1 小时前
state 状态相关
前端
梦之云1 小时前
effect 副作用相关
前端
golang学习记1 小时前
从0死磕全栈之Next.js 生产环境优化最佳实践
前端
Mintopia1 小时前
🧠 Next.js 还是 Nuxt.js?——当 JavaScript 碰上命运的分叉路
前端·后端·全栈
5pace2 小时前
Mac Nginx安装、启动、简单命令(苍穹外卖、黑马点评前端环境搭建)
java·前端·nginx·macos·tomcat
Learn Beyond Limits2 小时前
如何在Mac进行Safari网页长截图?
前端·macos·safari·方法·操作·功能·开发者平台