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

相关推荐
CQ_YM19 小时前
Linux进程终止
linux·服务器·前端·进程
晓得迷路了19 小时前
栗子前端技术周刊第 110 期 - shadcn/create、Github 更新 npm 令牌政策、Deno 2.6...
前端·javascript·css
nvd1119 小时前
GKE web 应用实现 Auth0 + GitHub OAuth 2.0登录实施指南
前端·github
前端小端长19 小时前
项目里满是if-else?用这5招优化if-else让你的代码清爽到飞起
开发语言·前端·javascript
胡萝卜3.019 小时前
现代C++特性深度探索:模板扩展、类增强、STL更新与Lambda表达式
服务器·开发语言·前端·c++·人工智能·lambda·移动构造和移动赋值
AI_567819 小时前
Vue3组件通信的实战指南
前端·javascript·vue.js
烤麻辣烫19 小时前
黑马大事件学习-16 (前端主页面)
前端·css·vue.js·学习
Dragon Wu19 小时前
TanStack Query(React Query) 使用总结
前端·react.js·前端框架·react
鹏多多19 小时前
flutter-使用EventBus实现组件间数据通信
android·前端·flutter
UpgradeLink19 小时前
Electron项目使用electron-updater与UpgradeLink接入参考
开发语言·前端·javascript·笔记·electron·用户运营