table 组件了解两下?

4,715 阅读9分钟

前言

午后写的这篇文章,微风加阳光,很是惬意🏖。
然后,话不多说,我们直接进入正题,今天主要会讲解一下可伸缩列、固定列、多级表头和几个表格的常见问题✍️,干货满满哦😯。

温馨提示:本篇文章是继上次 table 组件了解一下? 这篇文章之后写的,所以建议先去阅读一下前面那篇文章👀。当然也可以直接往下看,因为这里主要说下思路,没放多少代码😁。

可伸缩列

可伸缩列,顾名思义就是我们可以通过拖动表头的 border 来实现列宽的大小,看看下面这张图你就懂了👇: 上图的意思应该表达的挺明了,现在我们简要看下具体实现步骤👇:

第一步:

我们在表头的每个 th 里面都多增加一个 div,也就是上图中红色的部分,然后绝对定位于 th 的右边即可。

第二步:

在表头 header 和表体 body 同级的地方增加一个 div 来表示上图中的虚线,默认是隐藏的,就像下面这张图:

第三步:

对红色部分的鼠标事件进行监听:当鼠标按下的时候,就显示虚线,并实时改变虚线的 left 值,当鼠标抬起的时候就隐藏虚线,并计算出拖拽后的列宽,之后修改的 columns 里面对应列的 width 即可,这样表头和表体的列宽都会同步改变。 当然,还要记得在鼠标抬起时解绑 mousemovemouseup 事件,这是个好习惯。
以上就是可伸缩列的实现方式。

固定列

接下来我们来看看固定列是怎么实现的。首先还是 api 的设计,这个应该容易想到,我们在 columns 里面把需要固定的列添加上 fixed 属性即可,它的值有两种选择(leftright),就像下面这样:

columns: [
    {
      title: '姓名',
      key: 'name',
      width: 100,
      fixed: 'left'
    }
    ...
]

为了简化问题,这里我们只考虑左列固定,因为右列固定也是一样的。
这里先一句话说明下原理😁:就是多渲染一份表格并绝对定位在原来的表格之上,下面这张图应该能帮助你理解👇: 其实上图已经是实现的核心了,So,我们就直接说下具体实现方式😎:

第一步:处理表格数据

因为要把需要固定的列放到左边,所以我们一开始最需要处理的就是表格数据,把所有有 fixed="left" 属性的列排在前面。

第二步:渲染两个表格

再来就是正常渲染两个表格(A 和 B),事实上表格 A 和表格 B 是一模一样的,只不过表格 B多设置了一些属性,比如绝对定位在左上角、定宽(宽度就是有fixed="left"属性的列的宽度之和)、溢出隐藏啊等。具体代码结构如下图所示: 此外,对于表格 A 的 fixed 部分我们可以设置 visibility: hidden,因为它不需要展示,而且 Element 也是这样写的;同样地,对于表格 B 的非 fixed 部分也可以设置 visibility: hidden
这时候我突然产生了一个疑问🤔,就是为什么要设置成 visibility: hidden 而不设置成 display: none 呢?display: none 难道不是可以渲染更少的 dom 吗?设置成 visibility 的意义在哪里?这是个不错的问题,建议大家思考一下下。。。再往下看😁。

带着疑问我顺便看了下 iView 和 Ant Design 的 dom 结构,发现 iView 也是用 visibility: hidden 来处理的,而 Ant Design 则是直接不渲染了,这就很奇怪啦!于是乎我把 iView 和 Element 的 visibility: hidden 换成 display: none 试了一下,发现好像也 ok,表格展示也是对的,没什么问题,所以是为什么呢?其实最主要的原因是把 visibility: hidden 换成 display: none 会引发行对不齐的问题。
什么意思呢?就是说如果我们设置 display: none 的话,表格 A 里面的行高是不固定的,但这时候表格 B 是没有展示表格 A 中表体的内容,所以表格 B 不能同步表格 A 里面的高;而如果我们设置成 visibility: hidden 的话,表格 A 和表格 B 其实是都包含所有数据的,只是视觉上不可见而已,这样它们的行高就能够保持完全一致,虽然会导致多余的 dom 元素。
那 Ant Design 为什么可以呢?其实 Ant Design 也是有这个问题的,虽然它没有渲染多余的 dom,但是它会事先计算出表格 A 的行高,然后在去同步设置固定列的行高,此外在表格大小、列宽变化的时候也要去同步。它们是两种不同的方案,大家自己好好体会一下🙌。
当然,这里我们采用的是 Element 和 iView 的方案。

第三步:同步滚动

上面的实现方式会有什么问题呢🤔?最明显的问题就是表格 A 和表格 B 是割裂的,所以滚动其中一个表格的时候,另外一个表格是不会跟着响应的。事实上每个表格里面的表头和表体也是割裂的,所以现在我们要做就是同步滚动:当我们横向滚动表格 A 的表体时,我们需要同步滚动表格 A 的表头部分;当我们纵向滚动表格 A 的表体时,我们需要同步滚动表格 B 的表体部分。
这里以表格 A 里面的表体(A__body)滚动为例子,简要说下具体做法: A__body 横向滚动时:获取 A__bodyscrollLeft 值,然后把值同步到表格 A 的表头; A__body 纵向滚动时:获取 A__bodyscrollTop 值,然后把值同步到表格 B 的表体。 当然滚动是一个高频动作,所以我们可以进行防抖处理;还可以尝试用 transform 来替代 scrollTopscrollLeft 进行滚动。

第四步:同步hover

同第三步一样,当我们 hover 每一行的时候也需要像滚动一样进行同步,也就是 hover 样式的同步。
同样地我们以 hover 表格 A 的表体为例🌰,监听行的 mouseentermouseleave 事件,当鼠标移入到某一行,这时候你是能获取该行信息的,然后同步到固定列的对应行即可;同理 hover 到固定列的时候也要同步到表格 A,这里就不再赘述。

多级表头

表头

首先还是 api 的设计,我们希望在 columns 里面加个 children 就能实现,就像下面这样(顺便看下多级表头长什么样):

columns: [
    {
      title: '日期',
      key: 'date',
      width: 200
    },
    {
      title: '配送信息',
      children: [
        {
          title: '姓名',
          key: 'name',
          width: 200
        },
        {
          title: '地址',
          key: 'addr'
        }
      ]
    }
]

从上图中我们可以看出多级表头其实是一行一行来渲染的,每行又可以分为一列一列来渲染,实际上就是一个二维数组,两个 v-for 循环,让我们截个 iView 的代码看看大体结构: 所以我们首先要做的就是对 columns 进行格式化,使其变成 下面这个样子(二维数组): 其中每个数据节点都会给个levelrowspancolspan,后面两个是用来实现合并单元格的,比如 columns 的第一项日期,它的 colspan 应该是它 children 的总数,如果没有 children 就是1;它的 rowspan 应该等于最大层数-当前层数+1,如果有 children 则为1。
这里可能会有点绕,所以需要停下来理一理思路🤔,但其实本质就是数据结构的转换,所以这里没有特别强调说明如何转换,大家自己动手试一试吧😊。

表体

表体渲染就简单了,只不过我们同样要对 columns 进行一下处理,我们先看看整理之后的列大概长什么样: 其实新的 columns 就是遍历一下旧的 columns,有 children 就继续遍历获取子列,没 children 就直接取出该列,类似于获取到所有叶子节点的列,这个新的列会代替原有的 columns 来渲染,由于这个数据结构转换简单点,所以我把代码贴了上来:

const getAllColumns = (cols, forTableHead = false) => {
    const columns = deepCopy(cols);
    const result = [];
    columns.forEach((column) => {
        if (column.children) {
            if (forTableHead) result.push(column);
            result.push.apply(result, getAllColumns(column.children, forTableHead));
        } else {
            result.push(column);
        }
    });
    return result;
};

其实还是个数据转换的问题,大家可以看下下面两张图加深理解👇: 至此,我们就把可伸缩列、固定列和多级表头的实现方式给扯完了,哈哈😁😁😁。

问题补充

1、列宽不一致

事实上我们的表格和表体的列宽其实还是不一致的,比如当表体有滚动条的时候,表头没有,所以就会出现一个滚动条宽度的差距,这时候通常的做法是当纵向滚动条存在的时就在表头末尾加上一列去弥补这个宽度差,一般我们称之为 gutter。那这个 gutter 的宽度值是多少呢?其实就是滚动条的宽度,那如何计算滚动条的宽度呢?大概原理就是创建一个 div,然后用(无 overflow: scroll 属性的 div 宽度)-(有 overflow: scrolldiv 宽度),就是滚动条的宽度。

2、错位

实际开发中,用 Element 的时候如果表格不知道为啥就错位了,可以尝试用以下方法解决:

this.$nextTick(() => {
    this.$refs['table'] && this.$refs['table'].doLayout()
})

3、卡顿

表格里如果满屏都是 tooltipinput 是会卡顿的,毕竟事件监听和 dom 结构都变多了,所以要避免这种情况的发生。比如行是编辑状态的时候才显示 inputtooltip 改成实时渲染,不要一开始把每个都加上 tooltip

4、key 值

v-for 的时候不要用 index 来绑定 key 值,因为 index 可能是会改变的,所以并不可靠,尽量用 id 来绑定。

5、大数据渲染

对于成千上万行的数据渲染,如果发生了卡顿是很正常的,这时候如果你的表格数据只是用来纯展示的话可以在组件中加上 functional: true 使其成为函数化组件,这样能减少渲染开销。另一种就是虚拟列表了,虽说表格数据成千上万,但可视区的数据就那么多、范围就那么大,我们永远只需要渲染可视区域和可视区上下附近的一部分数据即可,其他不用渲染,然后在滚动的时候根据滚动的多少来计算出当前要显示的数据区间即可,大体思路就是这个样子,当然了,It's easier said than done 😂。

小结

吼吼,至此,我们终于把 table 组件的常用功能点讲完了,说实话,是真的麻烦,不过希望能够对你有帮助,嘻嘻😄!