当前位置: 首页 > 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 = trueaxios.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 = 0endIndex.value = data.value.length}}console.log(virtualScrollView)// 如果数据的长度小于限制的长度,有虚拟滚动元素则移除if (virtualScrollView) {scrollbarView.removeChild(virtualScrollView)}})// 初始化虚拟滚动function initVirtualScroll() {// 如果没有超出限制,就不进行虚拟滚动if (data.value.length <= limit) returnconst { 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 已经为大家准备好了封装完善的虚拟表格组件。无论选择“自己动手”还是“直接用货”,关键在于理解虚拟滚动的原理,从而选出最适合你项目的方案。

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

相关文章:

虚拟表格实现全解析

在数据展示越来越复杂的今天&#xff0c;大量数据的渲染就像是“满汉全席”——如果把所有菜肴一次性摆上桌&#xff0c;既浪费资源也让人眼花缭乱。幸运的是&#xff0c;我们有两种选择&#xff1a; 自己动手&#xff1a;通过二次封装 Element Plus 的表格组件&#xff0c;实…...

使用 Grafana 监控 Spring Boot 应用

随着软件开发领域的不断发展&#xff0c;监控和可观测性已成为确保系统可靠性和性能的关键实践。Grafana 是一个功能强大的开源工具&#xff0c;能够为来自各种来源的监控数据提供丰富的可视化功能。在本篇博客中&#xff0c;我们将探讨如何将 Grafana 与 Spring Boot 应用程序…...

使用Socket编写超牛的http服务器和客户端(一)

实现一个高性能的基于 IOCP(I/O Completion Ports)的 HTTP 服务器,支持多线程、动态线程池调整和路由处理。 主要功能和特性 IOCP 模型: 使用多个 IOCP 句柄(IOCP_COUNT),将客户端连接均匀分配到不同的 IOCP 上,减少线程竞争。 工作线程使用 GetQueuedCompletionStatu…...

python turtle模块有哪几种命令

python turtle模块命令的分类&#xff1a; 1、运动命令 2、笔画控制命令 3、其他命令...

【Transformer架构】

目录 一、Transformer介绍 1.1 Transformer的诞生 1.2 什么是Transformer 1.3 Transformer的优势 1.4 Transformer的市场 二、Transformer架构 2.1 Transformer模型的作用 2.2 Transformer总体架构图 2.2.1 Transformer总体架构 2.2.2 输入部分 2.2.3 输出部分 2.2.…...

unity学习50:NavMeshAgent 区域Areas和cost

目录 1 NavMeshAgent 区域和成本的问题 2 区域Areas 2.1 区域和颜色 2.2 区域和成本 2.3 区域成本的作用 2.4 地图测试准备 2.5 如何实现 2.5.1 unity的2022之前的老版本 2.5.2 unity的2022之后的新版本 2.6 如果测试失败&#xff0c;是因为没有bake 2.7 测试前&…...

Blender小技巧和注意事项

1.雕刻模式如果没反应,需要将模式转换成编辑模式 2. 鼠标移到大纲 点击 小键盘的. / 大键盘句号 , 在大纲视图快速找到选中物体 3.打包图像等数据进Blender文件中,可以防止丢失 4.拍摄小物体用长焦镜头 , 焦距120mm左右...

Python常见面试题的详解15

1. 死锁&#xff08;Deadlock&#xff09; 死锁指的是在多线程或者多进程的运行环境中&#xff0c;两个或多个线程&#xff08;进程&#xff09;彼此等待对方释放所占用的资源&#xff0c;进而陷入无限期等待的僵局&#xff0c;最终导致程序无法继续推进。 必要条件 互斥条件…...

代码审计初探

学会了基础的代码审计后&#xff0c;就该提高一下了&#xff0c;学一下一些框架的php代码审计 先从一些小众的、已知存在漏洞的cms入手 phpems php的一款开源考试系统 源码下载 https://down.chinaz.com/soft/34597.htm 环境部署 windows审计&#xff0c;把相关文件放到phps…...

Spring面试题2

1、compareable和compactor区别 定义与包位置:Comparable是一个接口&#xff0c;位于java.lang包,需要类去实现接口&#xff1b;而Compactor是一个外部比较器&#xff0c;位于java.util包 用法&#xff1a;Comparable只需要实现int compareTo(T o) 方法&#xff0c;比较当前对…...

Linux 权限系统和软件安装(二):深入理解 Linux 权限系统

在 Linux 的世界里&#xff0c;权限系统犹如一位忠诚的卫士&#xff0c;严密守护着系统中的文件与目录&#xff0c;确保只有具备相应权限的用户才能进行操作。与其他一些操作系统不同&#xff0c;Linux 并不依据文件后缀名来标识文件的操作权限&#xff0c;而是构建了一套独特且…...

二:前端发送POST请求,后端获取数据

接着一&#xff1a;可以通过端口访问公网IP之后 二需要实现&#xff1a;点击飞书多维表格中的按钮&#xff0c;向服务器发送HTTP请求&#xff0c;并执行脚本程序 向服务器发送HTTP请求&#xff1a; 发送请求需要明确一下几个点 请求方法&#xff1a; 由于是向服务器端发送值…...

单机上使用docker搭建minio集群

单机上使用docker搭建minio集群 1.集群安装1.1前提条件1.2步骤指南1.2.1安装 Docker 和 Docker Compose&#xff08;如果尚未安装&#xff09;1.2.2编写docker-compose文件1.2.3启动1.2.4访问 2.使用2.1 mc客户端安装2.2创建一个连接2.3简单使用下 这里在ubuntu上单机安装一个m…...

安全生产月安全知识竞赛主持稿串词

女:尊敬的各位领导、各位来宾 男:各位参赛选手、观众朋友们 合:大家好&#xff5e; 女:安全是天&#xff0c;有了这一份天&#xff0c;我们的员工就会多一份幸福&#xff0c; 我们的企业就会多一丝光彩。 男:安全是地&#xff0c;有了这一片地&#xff0c;我们的员工就多了一…...

C++的设计模式

1. 创建型模式 单例模式 (Singleton) 意图&#xff1a;确保类仅有一个实例&#xff0c;并提供全局访问点。&#xff08;常见的日志类&#xff09;实现&#xff1a;class Singleton { private:static Singleton* instance;Singleton() {} // 私有构造函数 public:static Singl…...

C++手撕AVL树

C手撕AVL树 1、AVL树的概念2、AVL树的结构3、AVL树的插入3.1、大概过程3.2、更新平衡因子3.3、更新平衡因子代码3.4、左单旋3.5、右单旋3.6、右左双旋3.7、左右双旋 4、AVL树的删除5、AVL树的查找6、AVL树的平衡检测7、AVL树的其他函数完整代码 1、AVL树的概念 二叉搜索树虽可…...

写大论文的word版本格式整理,实现自动生成目录、参考文献序号、公式序号、图表序号

前情提要&#xff1a;最近开始写大论文&#xff0c;发现由于内容很多导致用老方法一个一个改的话超级麻烦&#xff0c;需要批量自动化处理&#xff0c;尤其是序号&#xff0c;在不断有增添删减的情况时序号手动调整很慢也容易出错&#xff0c;所以搞一个格式总结&#xff0c;记…...

Redission可重试、超时续约的实现原理(源码分析)

Redission遇到其他进程已经占用资源的时候会在指定时间waitTime内进行重试。实现过程如下&#xff1a; 执行获取锁的lua脚本时&#xff0c;会返回一个值&#xff0c; 如果获取锁成功&#xff0c;返回nil&#xff0c;也就是java里的null 如果获取锁失败&#xff0c;用语句“PT…...

java八股文-消息队列

一、MQ基础篇 1. 什么是消息队列&#xff1f; 消息队列&#xff08;MQ&#xff09;是分布式系统中实现异步通信的中间件&#xff0c;解耦生产者和消费者。 2. 使用场景有哪些&#xff1f; 异步处理&#xff08;如注册后发送邮件&#xff09;系统解耦&#xff08;不同服务通过…...

3分钟idea接入deepseek

DeepSeek简介 DeepSeek 是杭州深度求索人工智能基础技术研究有限公司开发的一系列大语言模型&#xff0c;背后是知名量化资管巨头幻方量化3。它专注于开发先进的大语言模型和相关技术&#xff0c;拥有多个版本的模型&#xff0c;如 DeepSeek-LLM、DeepSeek-V2、DeepSeek-V3 等&…...

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:

在 HarmonyOS 应用开发中&#xff0c;手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力&#xff0c;既支持点击、长按、拖拽等基础单一手势的精细控制&#xff0c;也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档&#xff0c…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

什么是EULA和DPA

文章目录 EULA&#xff08;End User License Agreement&#xff09;DPA&#xff08;Data Protection Agreement&#xff09;一、定义与背景二、核心内容三、法律效力与责任四、实际应用与意义 EULA&#xff08;End User License Agreement&#xff09; 定义&#xff1a; EULA即…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心

当仓库学会“思考”&#xff0c;物流的终极形态正在诞生 想象这样的场景&#xff1a; 凌晨3点&#xff0c;某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径&#xff1b;AI视觉系统在0.1秒内扫描包裹信息&#xff1b;数字孪生平台正模拟次日峰值流量压力…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

Android第十三次面试总结(四大 组件基础)

Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成&#xff0c;用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机&#xff1a; ​onCreate()​​ ​调用时机​&#xff1a;Activity 首次创建时调用。​…...