缘起

标题有点夸张,并不是完全放弃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. shullfe机制详解

    一.shuffle机制概述 shuffle机制就是发生在MR程序中,Mapper之后,Reducer之前的一系列分区排序的操作.shuffle的作用是为了保证Reducer收到的数据都是按键排序的. ...

  2. Mac安装软件包管理工具Homebrew

    PS:最近开始学习groovy,打算去官网下载SDK Bundle,可是官网半天加载不出来,而且莫名其妙就是下载不下来,Folx一直提示 "无效的HTTP相应:禁止",可能是插件和 ...

  3. ubantu10.04安装ns-2.34

    LQ大神说是这个搭配才能完美移植leach 安装如下: 1. 安装必须的软件,因为版本较久远, sudo gedit /etc/apt/sources.list(大概是个意思) 把里面的内容换成: d ...

  4. 2017-2018 ACM-ICPC, NEERC, Southern Subregional Contest

    A. Automatic Door 对于规律的点可以推公式计算,对于噪点则暴力计算,时间复杂度$O(m\log m)$. #include<stdio.h> #include<ios ...

  5. Financial Management POJ - 1004

    Financial Management POJ - 1004 解题思路:水题. #include <iostream> #include <cstdio> #include ...

  6. TextInputLayout 用法

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...

  7. Pandora 生成 Token

    生成 token 打数据到仓库 通过 api 签名工具实现 最后通过curl -XPOST -H "Content-Type: application/json" -H " ...

  8. 剑指offer——python【第23题】二叉搜索树的后序遍历序列

    题目描述 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果.如果是则输出Yes,否则输出No.假设输入的数组的任意两个数字都互不相同. 解题思路 首先要清楚,这道题不是让你去判断一个给定 ...

  9. IntelliJ IDEA 2017.2.6 x64 配置 tomcat 启动 maven 项目

    IntelliJ IDEA 2017.2.6 x64 配置 tomcat 启动 maven 项目 1.确认 IDEA 是否启用了 tomcat 插件 2.添加 tomcat 选择 tomcat 存放路 ...

  10. 防止enter提交表单

    如何防止回车(enter)键提交表单,其实很简单,就一句话.onkeydown="if(event.keyCode==13)return false;"把这句写在from标签里面就 ...