缘起

标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了。即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反而有所上升。

客观地说,antd是开源的,UI设计得比较美观(甩出其他组件库一条街),而且是蚂蚁金服的体验技术部(一堆p7,p8,p9,基本都是大牛级的)在持续地开发维护,质量可以信任。

不过,antd虽好,但一些组件在某一些场景下,是很不适用的。例如,以表格形式无限滚动地展示大量数据(1w+)时,antd-table就特别蹩脚了,光是首次渲染就能卡个五秒白屏。如果这个表格还要求能编辑,甚至不同列之间发生联动呢?对不起,antd-table无能为力,会把页面卡炸的。

antd-table本身是基于rc-table的扩展,而rc-table所属的react-component素来有自己的主张,在react社区其他的组件库都支持无限滚动时(例如react-data-grid, react-virtualized, react-tabulator..),很抱歉,它不支持。

爹爹不支持,作为儿女的antd-table也不好反对,顺其自然咯。

于是,部分使用antd的开发者就脑阔疼了,想使用其他支持无限滚动的表格组件吧,会发现诸多的问题:

1.UI太丑,真的,特别是react-data-grid,不能再丑了。虽然它的功能很强大,但颜值是个硬伤。想给它整容,符合antd一惯的审美风格,还真的挺繁杂的,从上手到放弃系列。

2.扩展起来,不接地气。有的组件库,功能很强,但封装得太厉害,说的就是上面的react-data-grid,还有react-tabulator,要想用起来,可不容易。说是react组件,可怎么用都觉得是反react,有点jq的倾向,惹不起。

3.文档的可读性差。react-data-grid,react-virtualized好歹还有基础的API文档,虽然写的不咋地,但也比react-tabulator这个只能让人去看源码的强。

4.版本不稳定。react-tabulator很任性,release直接从2,x升级到4.x...

5.不支持树形表格编辑。说的是react-virtualized,或许新版本支持了,但不得不对它说抱歉。

6.圈子不活跃,人少。人少、不活跃就意味着这个库可能不长久,比如react-tabulator。

一番比较下来,你会发现,还是react-component舒服,文档友好,扩展灵活,版本稳定,社区活跃,完全可以嵌套和插入自己写的react组件(就是丑了点),想必这也是antd基于它来做扩展的一个重要考量。antd或许是意识到了无限滚动地重要性,比如移动端的瀑布流,PC端商品列表的无限下拉刷新,在3.x版本已经基于react-data-grid做了一层扩展,增加了List组件,用来支持无限滚动。

但,对于表格而言,还是没有人性化的解决方案。

没办法,需求来了,不上也得上,自己手写一个吧。

目前为止,无限滚动没去做,只做了纵向虚拟滚动,滚动有些许延迟,但首次渲染和编辑的实时响应,还是可以接受的,而且支持固定左右列,横向滚动,完全支持自定义react组件的嵌套和插入,扩展起来太容易了。基本支持antd-table的用法。

实战

在动手写之前,要考虑一些问题:

1.是采用原生table,还是用div来模拟?

2.对于树形表格,采取怎样的虚拟滚动方案?

3.组件的职责边界怎么界定?

一、原生table Vs div模拟表格

table之所以叫table,用意很明显了,在你想要以表格形式展示数据的时候,首先要想到的,就是用table。

table布局有浏览器的特定算法实现加速绘制,且对静态表格来说,页面结构是很稳定的。

虽然div模拟表格绘制的速度也不慢,但要达到跟静态表格一样的结构稳定性,可就做许多额外的维护工作了,css辅助,js控制,浏览器背后对table做的脏活累活,你基本都得接手,从零开始。

但table也有硬伤,首先是样式不好自定义,想改装原生table,让它变得好看,还真不是一件快活的事,具体参考antd-table。其次,如果要求表格左右列能固定,中间列可滚动,原生table就很绝望了,它不得不多叫来两个table兄弟,让他们来辅佐自己,一个在左,一个在右,跟自己装载同样多的数据,但却只显示固定列。三兄弟之间,还要时不时保持联络,确保大家每行高度都是一样的。

如果这中间出了什么偏差,就会导致滚动的表格看起来左边或右边的行像是掉了下来....用过antd-table的人,应该会有这样的体会。

而div模拟表格就不一样了,它是从零开始的,一张白纸,想怎么画就怎么画,要多美就能多美。

要实现左右固定列滚动也不必装载三份一模一样的数据,一份就够了,它要做的,仅仅是把列固定,将固定列邻居的位置计算好,就能达到同样的效果。

这里,想看示例,可以看看阿里这位大爷写的div模拟表格

基于这个角度的比较,我得给div模拟表格投一票。

二、虚拟滚动方案

首先,得先理解虚拟滚动的概念。

滚动,相信大家都了解,无非就是块级盒子的内容长度或宽度超出了盒子的宽高,盒子若设置了溢出内容可滚动,那我们就会看到滚动条,可滚动的距离,跟溢出内容所占的长度或宽度是相等的。

 <div style="height:30px;overflow:scroll">
<p style="height: 10px">1</p>
<p style="height: 10px">2</p>
<p style="height: 10px">3</p>
<p style="height: 10px">4</p>
<p style="height: 10px">5</p>
<p style="height: 10px">6</p>
</div>

如上述例子,4、5、6是溢出的。它们的高度是30px,即可滚动的距离。

可以预见,如果还有7、8、9…9999等等近一万条数据,那么这个div同一时刻,最多只能展示4条数据,剩下的9997条数据,都需要滚动才能看到。

创建一个dom节点,成本完全能接受,十个百个千个也可以接受,但上万数十万呢?就算能接受,也不该如此浪费。

既然只能在同一时刻看到4个节点,为什么不能只创建4个节点,剩下的节点都是通过滚动要展现的时候,才去创建呢?

这自然是可以的。

虚拟滚动,就是出于这个目的来设计的。

假设数据有6条,这里只讨论高度。

如果只创建4个节点,马上就会发现,滚动条能滚动的距离不对,只有10px。与预期的30px不符。这是因为,滚动距离是浏览器根据盒子和盒子里的节点的高度计算出来的。我们只能调整节点的高度,无法直接修改滚动距离的值。

我们可以通过在后面创建一个辅助节点,将高度设为20px来解决这个问题。

 <div style="height:30px;overflow:scroll">
<p style="height: 10px">1</p>
<p style="height: 10px">2</p>
<p style="height: 10px">3</p>
<p style="height: 10px">4</p>
<p style="height: 20px">占位符</p>
</div>

现在,通过监听div的滚动事件,我们可以知道滚动条滚到了哪个位置,通过计算,得知展示的第一条数据在所有数据中,处于哪个位置,是第2条,还是第1条等等信息...

然后,进一步得知,哪一个未创建的节点,要立即被创建,并且,占位符的高度要对应变化。

例如上述例子里,展示2345的时候,占位符高度就要设为10px,并且最上面也要设置一个10px高的占位符,如:

 <div style="height:30px;overflow:scroll">
<p style="height: 10px">占位符</p>
<p style="height: 10px">2</p>
<p style="height: 10px">3</p>
<p style="height: 10px">4</p>
<p style="height: 10px">5</p>
<p style="height: 10px">占位符</p>
</div>

遵循的原则就是,确保2345节点(我们称之为视图区)的高度,与占位符的高度加起来,等于总数据的实际总高度。

因此引申出的一个问题就是,每个节点的高度得固定(在表格里,就是固定表格行高)。或者,至少是在彻底展示完成之前,计算出实际高度。前面讨论过的组件库,除了react-data-grid,没有哪个不是固定行高的。

并且,视图区的高度也要指定。

如此一来,有了这些不变高度的数值,就能通过监听滚动来计算上下占位符各自的高度。

虚拟滚动的效果,也就达成了。剩下都是优化的工作,例如缓存节点,diff计算每次滚动时要改变的节点等等。

到这里,我们已经得出了扁平数据列表的虚拟滚动方案。

那么树形表格呢?

树形表格,准确的说,指的是数据在表格中以树形的形式来展现。这样的表格,可以展开/收起父节点,并且可以嵌套无限层级。参考antd-table的例子

让树形表格支持虚拟滚动,可以利用刚才讨论的虚拟滚动方案。

这里的关键点在于,树形数据,是有父子层级关系的,并不是扁平数据。

因而首先要做的,就是把树形数据按顺序遍历平铺展开,即扁平化。

// 树形数据
const tree = [{
node: 1,
children: [{
node: 11,
children: []
}, {
node: 12,
children: []
}]
}, {
node: 2,
children: []
}, {
node: 3,
children: []
}] // 树形数据按顺序平铺展开
const flatten = [{
node: 1
}, {
node: 11
}, {
node: 12
}], {
node: 2
}], {
node: 3
}]]

如此一来,我们就可以完全复用讨论过的虚拟滚动方案,达成树形表格虚拟滚动的效果。

其次,树形表格的展现,一般是要根据层级的深度来缩进的,这样才美观。我们可以展开树形数据的时候,将层级深度记录下来,在创建节点的时候,根据层级深度来决定缩进的宽度。

这里,会遇到一些样式上的问题,比如展开图标、缩进的宽度,有可能会受到css规则的影响,使得实际效果与预期不符,这个就需要自己去排查解决了。

三、组件的职责边界

上面已经提到如何实现一个虚拟滚动的树形表格,但没提到树形表格怎么展开、收起子元素,更没提到表格的可编辑功能。

这涉及到组件职责边界的确定,也是现在要讨论的。

一个组件,特别是react组件,它应该有什么样的功能,能提供什么样的API以供扩展,是要考虑清楚的。考虑不清楚的,就像react-tabulator,写个自定义单元格编辑器都得寻找dom节点,跟JQ有什么区别,而且还要按照它们定的规则来写,否则就不起作用。

理想的组件,不应该附加额外的规则,而是利用现有的规则,加以合适的运行机制,来达到方便扩展的目的。

antd-table这点做的还算可以,我们只需要将自己的react组件跟提供的API对接,就能达成想要的效果。

所以,我们来确定一下虚拟滚动的树形表格,应该有怎样的职责边界。

首先,列出这表格该有的基础功能:

1.支持虚拟滚动

2.支持单元格自定义--任何dom节点或者react组件

3.支持左右列固定

没错,跟antd-table相比,只是多出了一个虚拟滚动。除此以外的其他功能,都应该是由表格的使用者来实现,诸如可编辑单元格,树形表格如何展开收起。

这些,可用一句话来总结——数据驱动视图。

如果用过D3,相信非常能理解这个理念。数据千变万化,组件的功能也能千变万化,这是很理想的状态。

这三个基础功能里,第1个可以采用上述的虚拟滚动方案来实现。第3个可以用css的sticky属性配合js计算来实现(具体不赘述,参考阿里大爷的例子)。

第2个,其实倒是最简单的了。

只需要用React编写每个单元格容器,就能做到支持单元格的自定义。因为react天生支持dom节点的嵌套,更是本身就支持react组件之间的互相组合。

到此,基于React手写一个虚拟滚动的表格,已经Over。

行动力强的读者,应该已经可以写出自己的demo了。

我写的表格例子,内部大概长这样:

      <Table onScroll={this.onScroll} style={{ maxHeight: this.tableHeight }}>
<TableHead
data={data}
columns={dataColumns}
rowWidth={this.rowWidth}
rowKey={this.rowKey}
onExpand={this.props.onExpand}
/>
<Placeholder
line={viewUpData.length}
height={this.cellHeight * viewUpData.length + 'px'}
/>
<ViewPort
data={data}
columns={dataColumns}
rowWidth={this.rowWidth}
rowKey={this.rowKey}
onExpand={this.props.onExpand}
/>
<Placeholder
line={viewDownData.length}
height={this.cellHeight * viewDownData.length + 'px'}
/>
</Table>

外部使用虚拟滚动表格,大概是这样:

          <VirtualTable
bordered
expandedRowKeys={expandedKeys}
rowKey="id"
onExpand={(expanded, record) => { this.onExpand(expanded, record) }}
dataSource={dataSource}
pagination={false}
scroll={{ y: 250 }}
columns={columns}
viewLine={7}
onBeforeScroll={this.onBeforeScroll}
/>

如果之前使用了antd-table来实现功能,那么,只需要将antd-table换成虚拟滚动表格,再加个视图区的限定于滚动监听,就完全OK了,不用改变任何原有的业务逻辑。

后续

数据驱动视图理念的瓶颈,限于我的有限知识,认为应是在于海量数据频繁快速变化的时候,渲染视图的速度如何能跟上来,怎样做到让人觉得画面流畅,完全不卡。

比如100万条数据的下拉滚动。

学海无涯,苦作舟。这条路,一直是会有苦的...

放弃antd table,基于React手写一个虚拟滚动的表格的更多相关文章

  1. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  2. 基于Vue手写一个下拉刷新

    当然不乏有很多下拉刷新的插件可以直接使用,但是自定义程度不强,大部分都只能改改文字,很难满足设计师的创意,譬如淘宝和京东首页那种效果,就需要自己花心思倒腾了,最近刚好有这种需求,做完了稍微总结一下,具 ...

  3. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  4. 浅析MyBatis(二):手写一个自己的MyBatis简单框架

    在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ...

  5. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  6. webview的简单介绍和手写一个H5套壳的webview

    1.webview是什么?作用是什么?和浏览器有什么关系? Webview 是一个基于webkit引擎,可以解析DOM 元素,展示html页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做 ...

  7. 摊牌了!我要手写一个“Spring Boot”

    目前的话,已经把 Spring MVC 相关常用的注解比如@GetMapping .@PostMapping .@PathVariable 写完了.我也已经将项目开源出来了,地址:https://gi ...

  8. 手写一个LRU工具类

    LRU概述 LRU算法,即最近最少使用算法.其使用场景非常广泛,像我们日常用的手机的后台应用展示,软件的复制粘贴板等. 本文将基于算法思想手写一个具有LRU算法功能的Java工具类. 结构设计 在插入 ...

  9. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

随机推荐

  1. Codeforces 840C On the Bench dp

    On the Bench 两个数如果所有质因子的奇偶性相同则是同一个数,问题就变成了给你n个数, 相同数字不能相邻的方案数. dp[ i ][ j ]表示前 i 种数字已经处理完, 还有 j 个位置需 ...

  2. phpstorm对laravel的一些使用技巧

    安装laravel插件,设置ctrl+alt+s 二 安装智能提示插件 composer require barryvdh/laravel-ide-helper 在config/app.php的pro ...

  3. Azure Database for MySQL 报 Please specify SSL options and retry.

    Exception has been thrown by the aspect of an invocation. ---> Authentication to host 'xxx.mysql. ...

  4. php接入支付宝的流程(转载)

    php接入支付宝的流程写在这里供像我一样的小白参考. 1.首先要有一个创建一个应用(选好自己想要的功能,关于支付的功能,貌似都需要签约) 2.下载SDK&Dome(网址https://doc. ...

  5. 关于FastReport在winform中的使用(包含FastReport.net的安装步骤链接)

    一.FastReport的简介 FastReport是功能齐全的报表控件,使开发者可以快速并高效地为·NET/VCL/COM/ActiveX应用程序添加报表支持. 二.FastReport的安装(推荐 ...

  6. python移植环境

    如果整理材料的时候或者给别人共享代码的时候,除了使用docker外,也可以使用pip或者conda生成依赖项文件,然后在其他机器上将该依赖项一一安装就可以了. 但是有很多版本的依赖导致使用pip总是安 ...

  7. 软件工程团队:Spring计划会议及详细计划表

     极限挑战! 小组Spring计划表: 11.15 进行软件需求分析,了解调查社会背景,确定要编写的软件,分配各小组成员的任务.确定小组会议每天召开地点时间. 3h 11.16 将任务进一步精确分配, ...

  8. git Disconnected:No supported authentication methods available问题解决

    在本地克隆gitlab上的项目,报如下错误:Disconnected:No supported authentication methods available(server sent:publick ...

  9. [jzoj]2505.【NOIP2011模拟7.29】藤原妹红

    Link https://jzoj.net/senior/#main/show/2505 Description 在幻想乡,藤原妹红是拥有不老不死能力的人类.虽然不喜欢与人们交流,妹红仍然保护着误入迷 ...

  10. HBase RegionServer Pause for hours 卡顿几小时 故障

    关键词:hbase jvm gc regionserver wal pause 背景: HBase 1.1.2 客户的hbase集群最近出现RegionServer宕机情况.跟踪了master和RS日 ...