放弃antd table,基于React手写一个虚拟滚动的表格

缘起css

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

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

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

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

 爹爹不支持,做为儿女的antd-table也很差反对,顺其天然咯。github

因而,部分使用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这个只能让人去看源码的强。antd

  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.组件的职责边界怎么界定?

1、原生table Vs div模拟表格

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

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

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

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

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

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

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

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

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

 

2、虚拟滚动方案

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

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

 <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>

 

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

能够预见,若是还有七、八、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规则的影响,使得实际效果与预期不符,这个就须要本身去排查解决了。

 

3、组件的职责边界

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

这涉及到组件职责边界的肯定,也是如今要讨论的。

一个组件,特别是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万条数据的下拉滚动。

 

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

相关文章
相关标签/搜索