vue3 基于 el-table 的无限滚动自定义指令实现

el-table 虚拟滚动完整实现方案

方案概述

本方案通过自定义指令 v-infinite-scroll 实现 el-table 组件的虚拟滚动功能,优化大数据量下的表格性能。核心原理是只渲染可视区域及缓冲区域的数据,通过 padding 撑开滚动条来模拟完整数据的高度。

核心特性

  • 性能优化:只渲染必要数据,避免大量 DOM 节点创建
  • 正确滚动:通过 padding 技术维持滚动条行为正确
  • 易于集成:通过指令方式简单使用
  • 动态数据:支持异步数据加载和动态更新

实现代码

自定义指令实现

ini 复制代码
/**
 * el-table 虚拟滚动指令
 * 通过只渲染可视区域+缓冲区域的数据来优化大数据量下的性能
 */
export const infiniteScroll = {
  /**
   * 指令挂载时初始化虚拟滚动功能
   * @param {HTMLElement} el - 绑定指令的元素
   * @param {Object} binding - 指令绑定值
   */
  mounted(el, binding) {
    // 解构指令参数
    const { rowHeight, bufferCount = 5, dataArray, onScroll } = binding.value;
    let ticking = false; // 节流锁,确保每帧只执行一次更新
​
    // 获取真实的滚动容器和表格元素
    const container = el.querySelector('.el-scrollbar__wrap');
    const table = container.querySelector('table');
​
    /**
     * 获取当前数据总量
     * 通过传递的数组引用动态获取长度,确保数据更新时能正确响应
     */
    const getTotalCount = () => dataArray ? dataArray.length : 0;
​
    /**
     * 更新可视区域数据
     * 核心函数:计算当前可视区域索引,设置padding维持滚动条,通知父组件更新数据
     */
    function update() {
      // 获取滚动位置和容器高度
      const scrollTop = container.scrollTop;
      const containerHeight = container.clientHeight;
      const totalCount = getTotalCount();
​
      // 计算可视区域索引
      // startIndex: 可视区域顶部所在的行索引
      let startIndex = Math.floor(scrollTop / rowHeight);
      // endIndex: 可视区域底部所在的行索引
      let endIndex = Math.ceil((scrollTop + containerHeight) / rowHeight) - 1;
​
      // 添加缓冲区域,提高滚动体验
      startIndex -= bufferCount;
      endIndex += bufferCount;
​
      // 基本边界处理
      startIndex = Math.max(0, startIndex);
      endIndex = Math.max(0, Math.min(totalCount - 1, endIndex));
​
      // 计算 padding 值来维持正确的滚动条高度
      // topPadding: 隐藏在上方的数据所占高度
      const topPadding = Math.max(0, startIndex * rowHeight);
      
      // bottomPadding: 隐藏在下方的数据所占高度
      // 减1是因为要从索引转换为剩余行数
      const bottomPadding = totalCount > 0 && endIndex >= 0 ? 
        (totalCount - endIndex - 1) * rowHeight : 0;
​
      // 通过 padding 撑开表格,模拟完整数据的高度
      table.style.paddingTop = `${topPadding}px`;
      table.style.paddingBottom = `${bottomPadding}px`;
​
      // 通知父组件更新显示数据
      if (typeof onScroll === 'function') {
        onScroll(startIndex, endIndex);
      }
​
      // 释放节流锁
      ticking = false;
    }
​
    /**
     * 使用 requestAnimationFrame 进行帧级节流
     * 确保每帧只执行一次更新,避免频繁计算影响性能
     */
    function requestTick() {
      if (!ticking) {
        requestAnimationFrame(update);
        ticking = true;
      }
    }
​
    // 监听滚动事件
    container.addEventListener('scroll', requestTick);
​
    // 初始化时直接通知父组件显示前20条数据
    // 这是一种实用的默认策略,虽然可能不完全准确但能保证基本显示效果
    if (typeof onScroll === 'function') {
      onScroll(0, 20);
    }
  }
};

使用示例

xml 复制代码
<template>
  <div class="virtual-table-demo">
    <h2>el-table 虚拟滚动演示</h2>
    
    <div class="controls">
      <el-button @click="loadData" type="primary">加载数据</el-button>
      <el-button @click="clearData" type="danger">清空数据</el-button>
      <el-button @click="generateLargeData" type="success">生成10万条数据</el-button>
    </div>
    
    <!-- 
      使用 v-infinite-scroll 指令实现虚拟滚动
      参数说明:
      - rowHeight: 每行高度(需固定)
      - bufferCount: 缓冲区域行数
      - dataArray: 数据数组引用(用于动态获取长度)
      - onScroll: 滚动时的回调函数
    -->
    <el-table 
      v-infinite-scroll="{
        rowHeight: 40,
        bufferCount: 5,
        dataArray: totalData,  // 传递数组引用而不是固定长度
        onScroll: handleScroll
      }"
      :data="displayData"
      height="400px"
      border
      v-loading="loading">
      
      <el-table-column prop="id" label="ID" width="80"></el-table-column>
      <el-table-column prop="name" label="姓名" width="150"></el-table-column>
      <el-table-column prop="email" label="邮箱"></el-table-column>
      <el-table-column prop="address" label="地址"></el-table-column>
      <el-table-column prop="date" label="日期" width="180"></el-table-column>
    </el-table>
    
    <div class="info">
      <p>总数据量: {{ totalData.length }}</p>
      <p>当前显示: {{ displayData.length }} 条</p>
      <p>显示范围: {{ visibleStartIndex }} - {{ visibleEndIndex }}</p>
    </div>
  </div>
</template>
​
<script setup>
import { ref, reactive, computed } from 'vue';
import { infiniteScroll } from './directives/infiniteScroll';
​
// 注册自定义指令
const vInfiniteScroll = infiniteScroll;
​
// 完整数据源
const totalData = reactive([]);
// 显示的数据(通过计算属性动态截取可视区域数据)
const displayData = computed(() => {
  return totalData.slice(visibleStartIndex.value, visibleEndIndex.value + 1);
});
​
// 当前显示范围索引
const visibleStartIndex = ref(0);
const visibleEndIndex = ref(0);
const loading = ref(false);
​
/**
 * 处理滚动事件回调
 * @param {number} startIndex - 可视区域起始索引
 * @param {number} endIndex - 可视区域结束索引
 */
const handleScroll = (startIndex, endIndex) => {
  visibleStartIndex.value = startIndex;
  visibleEndIndex.value = endIndex;
};
​
/**
 * 生成测试数据
 * @param {number} count - 数据量
 */
const generateData = (count) => {
  totalData.length = 0;
  
  for (let i = 0; i < count; i++) {
    totalData.push({
      id: i + 1,
      name: `用户${i + 1}`,
      email: `user${i + 1}@example.com`,
      address: `北京市朝阳区某某街道${i + 1}号`,
      date: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toLocaleDateString()
    });
  }
};
​
/**
 * 加载数据
 */
const loadData = async () => {
  loading.value = true;
  try {
    // 模拟API请求延迟
    await new Promise(resolve => setTimeout(resolve, 800));
    
    // 生成1万条测试数据
    generateData(10000);
    
    // 重置显示范围
    handleScroll(0, Math.min(20, totalData.length - 1));
  } finally {
    loading.value = false;
  }
};
​
/**
 * 生成大量数据测试性能
 */
const generateLargeData = async () => {
  loading.value = true;
  try {
    // 模拟API请求延迟
    await new Promise(resolve => setTimeout(resolve, 800));
    
    // 生成10万条测试数据
    generateData(100000);
    
    // 重置显示范围
    handleScroll(0, Math.min(20, totalData.length - 1));
  } finally {
    loading.value = false;
  }
};
​
/**
 * 清空数据
 */
const clearData = () => {
  totalData.length = 0;
  handleScroll(0, 0);
};
​
// 初始化少量数据用于测试
generateData(50);
handleScroll(0, 20);
</script>
​
<style scoped>
.virtual-table-demo {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
​
.controls {
  margin-bottom: 20px;
}
​
.controls .el-button {
  margin-right: 10px;
}
​
.info {
  margin: 20px 0;
  padding: 15px;
  background-color: #f5f5f5;
  border-radius: 4px;
  font-size: 14px;
}
​
.info p {
  margin: 8px 0;
}
</style>

使用说明

核心参数

  1. rowHeight: 每行固定高度(必须指定)
  2. bufferCount: 缓冲区域行数(默认5行)
  3. dataArray: 数据数组引用(用于动态获取长度)
  4. onScroll: 滚动回调函数

注意事项

  1. 行高固定:为保证计算准确性,需要固定每行高度
  2. 初始化策略:指令初始化时会默认显示前20条数据,这是一种实用但可能不完全准确的策略
相关推荐
CoolerWu1 小时前
TRAE SOLO实战:一个所见即所得的笔记软体
前端·trae
没落英雄1 小时前
简单了解 shadowDom
前端·html
BBB努力学习程序设计2 小时前
Bootstrap图片:让图片展示更优雅、更专业
前端·html
玉宇夕落2 小时前
深入理解 async/await:从原理到实战,彻底掌握 JavaScript 异步编程
前端
3秒一个大2 小时前
从代码示例看 ES8 中的 async/await:简化异步操作的利器
javascript
努力往上爬de蜗牛2 小时前
react native token失效 刷新机制
javascript·react native·react.js
Sailing2 小时前
🚀 Promise.then 与 async/await 到底差在哪?(这次彻底讲明白)
前端·javascript·面试
鹤鸣的日常2 小时前
Vue + element plus 二次封装表格
前端·javascript·vue.js·elementui·typescript
JarvanMo2 小时前
Flakeproof - 自动化 Flutter 的用户体验 (UX) 测试
前端