当前位置: 首页 > news >正文

虚拟表格实现全解析

在数据展示越来越复杂的今天,大量数据的渲染就像是“满汉全席”——如果把所有菜肴一次性摆上桌,既浪费资源也让人眼花缭乱。幸运的是,我们有两种选择:

  • 自己动手:通过二次封装 Element Plus 的表格组件,实现虚拟滚动,只渲染用户视野中的数据,确保性能丝滑。
  • 直接用货:直接使用 Element Plus 封装好的虚拟表格组件,省时省力,稳稳地解决问题

本文将主要讲解如何实现自己的虚拟表格,并对整个实现思路进行深度解析,同时友好地告诉你:如果懒得折腾,Element Plus 的组件已经为你准备好了完美方案!

1. 为什么需要虚拟表格?

当数据量较小时(例如 100 条以内),直接渲染 <el-table> 完全没有问题。但一旦数据量飙升到数千或上万条时,浏览器就可能因为渲染过多 DOM 节点而变得像卡住的老爷车。解决方案很简单:虚拟滚动。虚拟滚动技术只渲染当前可见区域的数据,而把其余数据“藏”起来,直到滚动时才动态加载,这就像只上桌当下你需要的菜,其余的保持在厨房中等待叫单

2. 实现思路与系统架构

我们采用基于 Element Plus 的二次封装方式,核心思路如下:

  • 页面组件 index.vue
    负责生成数据并调用接口,将数据传递给虚拟表格组件。

  • 虚拟表格组件 VirtualTable.vue
    在 Element Plus 的 <el-table> 基础上封装,接入自定义的虚拟滚动逻辑,动态调整渲染数据范围。

  • 核心逻辑 useTakeVirtualScroll.ts
    这是“魔术师”所在,通过监听滚动和数据变化,根据当前视口计算出需要展示的数据区间,仅渲染这一部分数据,从而大幅提升性能。

温馨提示:虽然本文详细介绍了如何实现虚拟表格,但如果你只是想快速搭建产品,也可以直接使用 Element Plus 封装好的虚拟表格组件,它已经集成了很多优化功能,无需额外开发!

3. 代码实现详解

3.1 页面组件 index.vue

这个组件负责生成数据并模拟接口请求,然后将数据传递给我们的虚拟表格组件。看代码就知道,点击按钮就像是向厨房下单,数据开始滚滚而来:

<template>
    <div>
        <el-button type="primary" @click="handleGenerateData(100)" :disabled="loading">生成100条数据</el-button>
        <el-button type="primary" @click="handleGenerateData(10000)" :disabled="loading">生成10000条数据</el-button>
        <el-text type="danger">
            超过100条数据后,开启虚拟滚动
        </el-text>
    </div>
    <div class="virtual-table">
        <Table :data="data" :columns="column" :loading="loading" height="100%">
            <template #operation>
                <el-link type="primary">编辑</el-link>
            </template>
        </Table>
    </div>
</template>

<script setup lang="ts">
import Table from '@/components/VirtualTable/index.vue'
import { column } from './ts/column'
import axios from 'axios'
import { ref } from 'vue'
const data = ref([])

const loading = ref(false)
// 模拟接口请求
function handleGenerateData(num: number) {
    loading.value = true
    axios.post('http://localhost:8050/generateData', { num }).then(res => {
        if (res.data.message === 'success') {
            data.value = res.data.data
        }
    }).finally(() => {
        loading.value = false
    })
}
</script>

<style scoped>
.virtual-table {
    width: 100%;
    height: calc(100% - 32px);
    padding-top: 10px;
    box-sizing: border-box;
}
</style>

3.2 虚拟表格组件 VirtualTable.vue

在这个组件中,我们利用 Element Plus 的 <el-table>,并引入 useTakeVirtualScroll 钩子来实现虚拟滚动。简而言之,它只负责展示当前可见的数据:

<template>
    <el-table :data="filterData" v-loading="loading" v-bind="$attrs" @scroll="handleScroll">
        <el-table-column v-for="column in columns" :key="column.prop" v-bind="column">
            <template v-if="column.slot" #default="{ row }">
                <slot :name="column.slot" :row="row" />
            </template>
        </el-table-column>
    </el-table>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import type { PropType } from 'vue'
import type { Column } from '@/views/VirtualTable/ts/column'
import { useTakeVirtualScroll } from '@/hooks/useTavkeVirtualScroll'
const props = defineProps({
    data: {
        type: Array,
        required: true,
        default: () => []
    },

    columns: {
        type: Array as PropType<Column[]>,
        required: true,
        default: () => []
    },
    loading: {
        type: Boolean,
        default: false
    },
    // 限制多少条后开启虚拟滚动
    limit: {
        type: Number,
        default: 100
    }
})
const data = computed(() => props.data)
const { filterData, handleScroll } = useTakeVirtualScroll(data, props.limit)

</script>

<style scoped>
::v-deep(.el-scrollbar__view .el-table__body) {
    position: sticky;
    top: 0;
    left: 0;
}
</style>

3.3 核心逻辑:虚拟滚动钩子 useTakeVirtualScroll.ts

这部分代码正是“幕后黑手”,它负责监听滚动事件和数据变化,根据当前滚动位置计算出需要展示的数据区间。代码精妙地保证了只渲染用户可见部分:

import { ref, watch, nextTick, computed } from 'vue'
import { useEventListener, useDebounceFn } from '@vueuse/core'
import type { Ref } from 'vue'

type FunctionType = (
  data: Ref<any[]>,
  limit: number,
) => { filterData: Ref<any[]>; handleScroll: (data: { scrollTop: number }) => void }



export const useTakeVirtualScroll: FunctionType = (data, limit) => {
  const startIndex = ref(0) // 起始索引
  const endIndex = ref(0) // 结束索引
  const rowHeight = ref(42) // 行高

  // 计算过滤后的数据
  const filterData = computed(() => data.value.slice(startIndex.value, endIndex.value))

  // 监听数据变化
  watch(data, async () => {
    const { tableView, virtualScrollView, scrollbarView } = getElement()
    if (data.value.length) {
      tableView.scrollTo(0, 0)
      // 如果数据的长度大于限制的长度,则初始化虚拟滚动
      if (data.value.length > limit) {
        await nextTick()
        initVirtualScroll()
        return
      } else {
        startIndex.value = 0
        endIndex.value = data.value.length
      }
    }
    console.log(virtualScrollView)
    // 如果数据的长度小于限制的长度,有虚拟滚动元素则移除
    if (virtualScrollView) {
      scrollbarView.removeChild(virtualScrollView)
    }
  })

  // 初始化虚拟滚动
  function initVirtualScroll() {
    // 如果没有超出限制,就不进行虚拟滚动
    if (data.value.length <= limit) return

    const { tableView, virtualScrollView, scrollbarView } = getElement()
    const tableRow = scrollbarView.querySelector('.el-table__row') as HTMLElement // 获取表格行
    rowHeight.value = tableRow?.clientHeight || 42 // 获取表格行高
    const tableViewHeight = tableView?.clientHeight // 获取表格可视窗口的高度
    const virtualScrollHeight = rowHeight.value * data.value.length // 根据数组的长度来计算表格需要滚动的虚拟高度

    // 计算当前滚动到的行索引以及可视行数
    setIndex(Math.floor(tableView.scrollTop / rowHeight.value), Math.ceil(tableViewHeight / rowHeight.value))

    // 如果存在虚拟滚动视图,则更新高度
    if (virtualScrollView) {
      virtualScrollView.style.height = `${virtualScrollHeight - tableViewHeight}px`
      return
    }

    // 创建一个元素
    const fragment = document.createDocumentFragment()
    // 创建一个虚拟高度的元素
    const virtualScrollViewElement = document.createElement('div')
    virtualScrollViewElement.classList.add('virtual-scroll-view')
    // 设置虚拟高度的元素高度需要减去表格的可视化的高度
    virtualScrollViewElement.style.height = `${virtualScrollHeight - tableViewHeight}px`
    fragment.appendChild(virtualScrollViewElement)
    // 将虚拟高度的元素添加到表格中
    scrollbarView.appendChild(fragment)

  }


  // 处理滚动
  function handleScroll({ scrollTop }: { scrollTop: number }) {
    if (data.value.length <= limit) {
      return
    }

    const { tableView } = getElement()
    const tableViewHeight = tableView?.clientHeight // 获取表格可视窗口的高度
    // 计算当前滚动到的行索引以及可视行数
    setIndex(Math.floor(scrollTop / rowHeight.value), Math.ceil(tableViewHeight / rowHeight.value))
  }


  // 获取想要的元素
  function getElement() {
    const tableView = document.querySelector('.el-scrollbar__wrap') as HTMLElement // 获取滚动容器
    const scrollbarView = document.querySelector('.el-scrollbar__view') as HTMLElement // 获取滚动视图
    const virtualScrollView = scrollbarView.querySelector('.virtual-scroll-view') as HTMLElement // 获取虚拟滚动视图

    return { tableView, virtualScrollView, scrollbarView }
  }

  // 设置索引
  function setIndex(start: number, end: number) {
    startIndex.value = Math.max(0, start)
    endIndex.value = Math.min(data.value.length, start + end)
  }


  const debouncedFn = useDebounceFn(initVirtualScroll, 100)

  useEventListener(window, 'resize', debouncedFn)

  return { filterData, handleScroll }
}

细解析:

  • 数据截取策略

    • 核心变量startIndexendIndex 分别定义了当前可见数据的起始与结束位置;rowHeight 则表示每一行的高度。
    • filterData 计算属性:借助 Vue 的响应式特性,filterData 始终返回 data 数组中从 startIndexendIndex 的部分,从而保证页面只渲染用户当前能看到的数据。
  • 数据监听与初始化

    • watch(data, async () => { ... }):每当数据发生变化时,先等待 DOM 更新(通过 nextTick()),再判断数据量是否超过设定阈值。
    • 若数据量超过 limit,则调用 initVirtualScroll() 进行初始化;否则直接显示全部数据。
    • 这种机制就像在超市里:当货架上的商品数量不多时,顾客可以一目了然;而一旦商品过多,则分区促销,只展示一部分热销品。
  • 初始化虚拟滚动

    • initVirtualScroll():首次加载或数据更新时,通过查询 DOM 获取表格容器(.el-scrollbar__wrap)的高度,根据当前滚动条位置计算出起始行和可见行数,并调用 setIndex() 更新数据区间。
    • 这确保了页面一加载时,就只显示当前视口内的数据,而不会一次性加载所有数据。
  • 滚动事件处理

    • handleScroll({ scrollTop }):每次用户滚动时,实时根据新的 scrollTop 值重新计算可见区域,并更新 startIndexendIndex
    • 这样,无论用户如何快速滚动,页面始终只渲染当前视口内的数据,保证流畅的滚动体验。
  • 更新显示数据区间

    • setIndex(start, end):确保更新后的 startIndex 不低于 0,endIndex 不超过数据总量。
    • 这一步防止了由于计算误差导致索引越界的情况,保证数据截取始终正确。
  • 防抖优化

    • useEventListener(window, 'resize', useDebounceFn(initVirtualScroll, 100)):在窗口大小变化时,防止因频繁触发初始化函数而带来的性能损耗。
    • 防抖函数确保只有在调整停止一段时间后才执行初始化,相当于给“表格魔术师”一点缓冲时间,避免过度“表演”。

4. 总结

本文深入解析了如何基于 Element Plus 的 <el-table> 组件,通过二次封装实现虚拟滚动表格。重点在于核心逻辑 useTakeVirtualScroll.ts

  • 利用 Vue 的响应式和 computed 属性,仅渲染用户当前视口内的数据。
  • 通过监听数据变化与滚动事件,动态计算并更新显示区间,确保页面渲染始终高效流畅。
  • 防抖优化进一步保障了在窗口调整等情况下的稳定性。

当然,如果你不想自己重造轮子,Element Plus 已经为大家准备好了封装完善的虚拟表格组件。无论选择“自己动手”还是“直接用货”,关键在于理解虚拟滚动的原理,从而选出最适合你项目的方案。

希望这篇文章既能帮你学会如何实现高性能的虚拟表格,又能在你选择方案时提供足够的参考。如果你有任何疑问或优化建议,欢迎留言交流,让我们一起玩转大数据渲染的世界!

相关文章:

  • 机器学习数学基础:29.t检验
  • 【新人系列】Python 入门(三十一):内存管理
  • Java中JDK、JRE,JVM之间的关系
  • 2025开源数据工程全景图
  • 【Java进阶学习 第一篇】Java中的继承
  • Ubuntu搭建RTSP服务器
  • DeepSeek学术指南:利用DeepSeek撰写学术论文和需要注意的问题
  • Git 中 rebase, squash, amend 的作⽤
  • 服务器配置-从0到分析2:服务器基本设置
  • 第4章 信息系统架构(四)
  • Git企业开发
  • 机器学习笔记——常用损失函数
  • python 神经网络教程,神经网络模型代码python,小白入门基础教程
  • 云端SaaS系统架构设计
  • Web入侵实战分析-常见web攻击类应急处置实验2
  • 【随手笔记】NB和4G信号杂记
  • golang--字符串处理(rune类型)
  • Qt开源项目获取
  • 【GORM学习笔记】GORM介绍以及增删改查相关操作
  • 算法的解题模式Ⅳ
  • 重庆找做墩子网站/seo云优化软件破解版
  • 网站主体负责人邮箱/搜索引擎推广的优势
  • 青岛提供网站建设哪家便宜/聊城疫情最新消息
  • 做家教中介网站赚钱吗/优化大师win10能用吗
  • 网站建设委托合同/优化网站的步骤
  • wordpress采集爬虫/seo赚钱培训课程