[React]如何提高大数据量场景下的Table性能?
[React]如何提高大数据量场景下的Table性能?
两个方向:虚拟列表,发布订阅
虚拟列表
虚拟列表实际上只对可视区域的数据项进行渲染
可视区域(visibleHeight)
: 根据屏幕可视区域动态计算或自定义固定高度数据渲染项(visibleCount)
:可视区域除以行高并向下取整startIndex
: 初始为0,听过滚动条偏移计算endIndex
:startIndex
+visibleCount
; 数据结束位置索引,可额外预加载几条数据
实现思路
监听逻辑实现
useEffect(() => {const onScrollChange = (e: React.WheelEvent) => {const top = (e.target as HTMLElement).scrollTopconst index = Math.floor(top / rowHeight)setScrollTop(top)setStartIndex(index ? index + 1 : 0)}virtualizedRef.current.addEventListener('scroll', onScrollChange)return () => {if (virtualizedRef.current) {virtualizedRef.current.removeEventListener('scroll', onScrollChange)}}}, [])
HTML结构如下:
virtualized_placeholder
: 容器内占位,高度为列表总高度,撑满父容器,用于可视区域形成滚动条
<div ref={virtualizedRef} style={{ height: visibleHeight }}><table style={{ transform: `translate3d(0px, ${scrollTop}px, 0)` }}><thead>{...}</thead><tbody>{...}</tbody></table><div className="virtualized_placeholder" style={{ height: placeHeight }} /></div>
主要逻辑
- 设置容器占位高度,计算可视区域数据项
- 监听容器滚动事件,计算偏移距离,startIndex,组件卸载移除滚动事件
- startIndex作为deps依赖项,当发生改变更新展示数据
useEffect(() => {const placeH = ((dataSource.length) * rowHeight) + rowHeightsetPlaceHeight(placeH)setVisibleCount(Math.floor(visibleHeight / rowHeight) + 2)}, [dataSource, rowHeight])useEffect(() => {const onScrollChange = (e: React.WheelEvent) => {const top = (e.target as HTMLElement).scrollTopconst index = Math.floor(top / rowHeight)setScrollTop(top)setStartIndex(index ? index + 1 : 0)}virtualizedRef.current.addEventListener('scroll', onScrollChange)return () => {if (virtualizedRef.current) {virtualizedRef.current.removeEventListener('scroll', onScrollChange)}}}, [])useEffect(() => {const data = dataSource.slice(startIndex, startIndex + visibleCount)setShowData(data)}, [startIndex, visibleCount, dataSource])
完整代码
/*** dataSource 数据数组 object[]* columns 表格列 string[]* rowKey 表格行key的取值 number | string* rowHeight tr固定高度 number* visibleHeight 可视区域高度 number* hasOrder 是否含有序号 boolean* orderTitle 序号标题 string*/
import React, { FC, useEffect, useState, useRef, memo } from 'react'
import { Empty } from 'antd';
import './index.less'interface DataProps {[key:string]: any
}
interface VirtualProps {dataSource: DataProps[]columns: string[]rowKey?: number | stringhasOrder?: booleanorderTitle?: stringrowHeight?: numbervisibleHeight?: number
}const Index: FC<VirtualProps> = (props) => {const {dataSource = [],columns = [],rowKey,hasOrder = false,orderTitle = '序号',rowHeight = 40,visibleHeight = 800,} = propsconst [startIndex, setStartIndex] = useState(0)const [placeHeight, setPlaceHeight] = useState(0)const [scrollTop, setScrollTop] = useState(0)const [visibleCount, setVisibleCount] = useState(0)const [showData, setShowData] = useState<DataProps[]>([])const virtualizedRef = useRef<any>(null)useEffect(() => {const placeH = ((dataSource.length) * rowHeight) + rowHeightsetPlaceHeight(placeH)setVisibleCount(Math.floor(visibleHeight / rowHeight) + 2)}, [dataSource, rowHeight])useEffect(() => {const onScrollChange = (e: React.WheelEvent) => {const top = (e.target as HTMLElement).scrollTopconst index = Math.floor(top / rowHeight)setScrollTop(top)setStartIndex(index ? index + 1 : 0)}virtualizedRef.current.addEventListener('scroll', onScrollChange)return () => {if (virtualizedRef.current) {virtualizedRef.current.removeEventListener('scroll', onScrollChange)}}}, [])useEffect(() => {const data = dataSource.slice(startIndex, startIndex + visibleCount)setShowData(data)}, [startIndex, visibleCount, dataSource])return (<div className="galois_virtualized_container" ref={virtualizedRef} style={{ height: visibleHeight }}><tablestyle={{ transform: `translate3d(0px, ${scrollTop}px, 0)` }}className="galois_virtualized_table"><thead><tr>{hasOrder && <th key="galois_index">{orderTitle}</th>}{columns.map(values => <th key={values}>{values}</th>)}</tr></thead><tbody>{showData.map((item, index) => (<tr key={rowKey ? item[rowKey] : index}>{hasOrder && <td>{startIndex + index + 1}</td>}{columns.map((values, ind) => <td key={ind}>{item[values]}</td>)}</tr>))}</tbody></table>{showData.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}<div className="galois_virtualized_placeholder" style={{ height: placeHeight }} /></div>)
}export default memo(Index)
利用发布订阅模式优化批量编辑的场景
正常情况下来说,把整个表格的数据都挂载到一个state中是最简单的,但是这么做的话,每次单元格在编辑onChange
的时候就会setState,从而更新整个table,在数据稍大的场景下,编辑的性能会非常低,用户每输入一个字都要rerender。
发布订阅可以帮我们去掉这一部分冗余的rerender,从而做到每个cell的onChange都是单独的。
预期下的单元格状态维护
每个cell都进行单独的状态管理,每个cell内部都是用
const [data, setData] = useState(defaultValue)return <Input value={data} onChange={(e)=>setData(e.target.value)}/>
来维护内容,这样的话即使onChange
,也只是rerender
这个单独的cell,不会影响到整个table。
发布订阅实现
export interface IRef {id: string[key: string]: any
}// 发布订阅模式
export class RefCollection {// 订阅者集合private refMap: Map<string, IRef>constructor() {this.refMap = new Map<string, IRef>()}// 添加订阅者public addRef(ref: IRef) {if (!this.refMap.has(ref.id)) {this.refMap.set(ref.id, ref)}}// 移除订阅者public removeRef(ref: IRef) {this.refMap.delete(ref.id)}// -----------------------下面是广播事件----------------------------------// 触发所有deps的submit方法public submit() {return Array.from(this.refMap.values()).map((oneRef => {return oneRef.submit?.()}))}// 触发所有deps的validate方法public validate() {return Array.from(this.refMap.values()).map((oneRef => {return oneRef.validate?.()}))}// ...其它
}
收集每个单元格的依赖
业务组件内:
// 注册一个收集器
const refCollection = useRef(new RefCollection())const columns = [{dataIndex: 'title',render(){return <Cell refCollection={refCollection}/>}}
]
单元格内部逻辑:
// 每一个Cell内const Cell = (props)=>{const { refCollection } = props// 每一个Cell内部自己实现接口,逻辑独立,只需关注自己即可const ref = useRef<any>({// 当前单元格的唯一标识id: uid(),// 这里随便加什么属性,可以加一些type来区别不同的Cell// 比如说有些是Select的控件,有些是Input的控件// 在submit的时候就可以根据type来过滤收集type: "inputRender",// 一般来说,可能要给定一个行号,因为我们提交数据的时候都是按行提交的// 有了行ID之后我们可以在submit的时候聚合数据,转换成需要提交的格式tableRowId: row?.tableRowId,validate: useMemoizedFn(() => {// 在这里实现自己的validate方法 // refCollection执行validate的时候会遍历每一个订阅者的validate方法// return boolean}),submit: useMemoizedFn(() => {// 在这里实现自己的submit方法// refCollection执行validate的时候会遍历每一个订阅者的submit方法// return {}}),})// 在这里收集依赖useEffect(() => {if (!refCollection) returnrefCollection?.add(ref.current)return () => {refCollection?.remove(ref.current)}}, [])return <div></div>
}
提交阶段
const refCollection = useRef(new RefCollection())const onSubmit = ()=>{await refCollection.current.validateAll()const data = refCollection.current.submit()// 提交逻辑 data
}
为什么不用FormItem?
- FormItem包含了其它很多逻辑,但是未必都需要用得上
- 如果一个单元格就要多渲染一层FormItem,整体下来就会非常地损耗性能
- FormItem如果不渲染出来,那么就无法做逻辑,而如果通过统一的状态管理,可以实现字段不渲染出来就能完成值的读取和修改,实现虚拟字段的效果(这时候可以搭配分页、虚拟列表提高性能),同时也能正常地兼顾一些联动操作(比如说表格数字汇总)
相关文章:
[React]如何提高大数据量场景下的Table性能?
[React]如何提高大数据量场景下的Table性能? 两个方向:虚拟列表,发布订阅 虚拟列表 虚拟列表实际上只对可视区域的数据项进行渲染 可视区域(visibleHeight): 根据屏幕可视区域动态计算或自定义固定高度数据渲染项&…...

基于Vision Transformer的mini_ImageNet图片分类实战
【图书推荐】《PyTorch深度学习与计算机视觉实践》-CSDN博客 PyTorch计算机视觉之Vision Transformer 整体结构-CSDN博客 mini_ImageNet数据集简介与下载 mini_ImageNet数据集节选自ImageNet数据集。ImageNet是一个非常有名的大型视觉数据集,它的建立旨在促进视觉…...
JS中map()使用记录
优点和缺点 总的来说,map() 方法是一个强大的工具,适合于需要将数组中的每个元素转换为新形式的情况。然而,对于性能敏感的应用或需要更复杂控制逻辑的场景,可能需要考虑其他方法。 优点: 函数式编程风格:…...

JavaWeb学习——请求响应、分层解耦
目录 一、请求响应学习 1、请求 简单参数 实体参数 数组集合参数 日期参数 Json参数 路径参数 总结 2、响应 ResponseBody&统一响应结果 二、分层解耦 1、三层架构 三层架构含义 架构划分 2、分层解耦 引入概念 容器认识 3、IOC&DI入门 4、IOC详解 …...
Vue中!.和?.是什么意思
在Vue(或更广泛地说,在JavaScript和TypeScript中),!. 和 ?. 是两个与可选链(Optional Chaining)和断言非空(Non-null Assertion)相关的操作符,它们分别用于处理可能为nu…...

秋招突击——7/22——复习{堆——前K个高频元素}——新作{回溯——单次搜索、分割回文串。链表——环形链表II,合并两个有序链表}
文章目录 引言复习堆堆——前K个高频元素个人实现复习实现二参考实现 新作单词搜索个人实现参考实现 分割回文串个人实现参考实现 环形链表II个人实现参考实现 两个有序链表个人实现 总结 引言 又是充满挑战性的一天,继续完成我们的任务吧!继续往下刷&a…...

android13禁用某个usb设备
总纲 android13 rom 开发总纲说明 目录 1.前言 2.触摸设备查看 3.功能修改 3.1 禁用usb触摸 3.2 禁用usb键盘 3.3 禁用usb遥感 4.查看生效与否 5.彩蛋 1.前言 用户想要禁止使用某些usb设备,需要系统不能使用相关的usb设备,例如usb触摸屏,usb键盘,usb遥感等等usb…...
tmux相关命令
tmux相关命令 1、tmux介绍2、会话(session)、窗口(windows)、窗格(pane)3、会话相关命令4、窗口相关命令5、窗格相关命令6、内容查看7、tmux配置文件 1、tmux介绍 略 2、会话(session…...
初创小程序公司怎么选服务器合作商
初创小程序公司怎么选服务器合作商?在移动互联网的浪潮中,小程序以其轻量、便捷、即用即走的特点,成为了众多初创企业快速触达用户、展现创意与服务的理想平台。然而,对于初创小程序公司而言,如何在纷繁复杂的服务器市…...

基于微信小程序+SpringBoot+Vue的自习室选座与门禁系统(带1w+文档)
基于微信小程序SpringBootVue的自习室选座与门禁系统(带1w文档) 基于微信小程序SpringBootVue的自习室选座与门禁系统(带1w文档) 本课题研究的研学自习室选座与门禁系统让用户在小程序端查看座位,预定座位,支付座位价格,该系统让用户预定座位…...

【Linux】进程IO|重定向|缓冲区|dup2|dup|用户级缓冲区|模拟缓冲区
目录 前言 重定向 实验一 为什么log.txt文件的文件描述符是1 为什么向stdout打印的信息也出现在文件中 实验二 用户级缓冲区 为什么要有用户级缓冲区 系统调用 dup 为什么close(fd1)之后还能向log.txt写入数据? dup2 缓冲区 观察现象 测试1 测试2 测…...
bug bug bug
importError: DLL load failed while importing _multiarray_umath: 找不到指定的模块。 Traceback (most recent call last): File "D:\yolov8_about\ultralytics-main3\trainCPU.py", line 4, in <module> from ultralytics import YOLO File "…...

医疗器械上市欧美,需要什么样的网络安全相关申报文件?
医疗器械在欧美上市时,需要提交的网络安全相关申报文件主要包括以下几个方面,这些要求基于欧美地区的法律法规和监管机构的指导文件。 一、美国FDA要求 1. 网络安全管理计划 内容:制造商需要提交一份网络安全管理计划,该计划应包含…...

【UbuntuDebian安装Nginx】在线安装Nginx
云计算:腾讯云轻量服务器 操作系统:Ubuntu-v22 1.更新系统软件包列表 打开终端并运行以下命令来确保你的系统软件包列表是最新的: sudo apt update2.安装 Nginx 使用以下命令安装 Nginx: sudo apt install nginx3.启动 Nginx…...

Jacoco 单元测试配置
前言 编写单元测试是开发健壮程序的有效途径,单元测试写的好不好可以从多个指标考量,其中一个就是单元测试的覆盖率。单元测试覆盖率可以看到我们的单元测试覆盖了多少代码行、类、分支等。查看单元测试覆盖率可以使用一些工具帮助我们计算,…...

App Instance 架构示例
前言 在Unity程序设计过程中,我们处理的第一个对象是Application Instance。 它的主要职责是启动流程管理、卸载流程管理,次要职责是管理在内部的子系统生命周期。其他职责,提供或桥接应用程序的配置信息、及其他第三方接口。 它通常以单例的…...

【论文速读】| MoRSE:利用检索增强生成技术填补网络安全专业知识的空白
本次分享论文:MoRSE: Bridging the Gap in Cybersecurity Expertise with Retrieval Augmented Generation 基本信息 原文作者:Marco Simoni, Andrea Saracino, Vinod Puthuvath, Maurco Conti 作者单位:意大利比萨国家研究委员会信息学与…...

pip install albumentations安装下载超级细水管
albumentations 是一个用于图像增强的 Python 库,它提供了丰富的图像变换功能,可以用于数据增强,从而提高深度学习模型的泛化能力。 直接安装命令: pip install albumentations但是如果半夜遇到这种19kB/s的下载速度 为头发着想&…...
驱动开发系列07 - 驱动程序如何分配内存
一:概述 Linux 内核提供了丰富的内存分配函数、在本文中,我们将介绍在设备驱动程序中分配和使用内存的方法,以及如何优化系统的内存资源。由于内核为驱动程序提供了统一的内存管理接口。所以我们不会去讨论不同架构是如何管理内存的,文本不涉及分段、分页等问题,此外在本文…...
【Jackson】注解及其使用
Jackson库提供了多种注解(annotations),可以用来控制JSON序列化和反序列化的行为。这些注解允许你灵活地映射Java对象与JSON数据之间的关系。下面将详细介绍一些常用的Jackson注解及其用法。 1. JsonProperty 作用: 用于指定JSON属性与Java…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...

渗透实战PortSwigger Labs指南:自定义标签XSS和SVG XSS利用
阻止除自定义标签之外的所有标签 先输入一些标签测试,说是全部标签都被禁了 除了自定义的 自定义<my-tag onmouseoveralert(xss)> <my-tag idx onfocusalert(document.cookie) tabindex1> onfocus 当元素获得焦点时(如通过点击或键盘导航&…...
基于 HTTP 的单向流式通信协议SSE详解
SSE(Server-Sent Events)详解 🧠 什么是 SSE? SSE(Server-Sent Events) 是 HTML5 标准中定义的一种通信机制,它允许服务器主动将事件推送给客户端(浏览器)。与传统的 H…...