NewTable技术说明

NewTable 是 Sugar Design 中的表格组件。

相关链接

一、NewTable的布局

NewTable是被tableWrapper所包裹的,tableWrapper的作用就是将NewTable与外界隔开,所有的外界样式作用于tableWrapper

tableWrapper中包含NewTable本身(tableContainer)和两个滚动条(横向 / 纵向)。

NewTable本身(tableContainer)包含3大块:

TableBody的内部则是由若干TableRow构成(图中未给出)。

还包含一些其他的元素,比如数据为空时,TableBodyTableFooter都会被EmptyContent (非NewTable的配置属性)所替换。

二、NewTable关键定义

2.1 基本含义

引出Table中最重要的4个变量(也是NewTable组件的state)的含义是什么。

图中浅灰色的矩形(较小的)就是Table在页面中所显示的大小,称之为视口宽高

而图中深灰色的内容则是Table本身数据全部展示需要的大小,称之为实际宽高

2.2 计算逻辑

视口宽高、实际宽高是贯穿整个NewTable绘制逻辑的变量,非常重要。下面来解释一下4个变量分别都是怎么取值的。

宽高的计算逻辑基本是一样的,所以就按照视口的计算逻辑和实际的计算逻辑2种方式来讲述。

2.2.1 视口宽高

与width和height两个配置项有关。

配置了width / height

NewTable第一次渲染时,会直接把width(height)作为CSS中对应的属性赋值给tableWrapper的样式然后渲染。

渲染过后会在componentDidMount中通过DOM操作拿到NewTable此时视口的宽高所对应的具体数值。

这么做是因为NewTable的渲染需要具体的数值,而width/height可能配置的值是一个类似于'100%'的字符串。

然后tableContainerHeight(or Width)的值只会在两种情况下再次改变:

没有配置了width / height

在没有给定width / height时,这是在告诉NewTable其宽高应该随内容完全撑开。也就是数据有多少行多少列,都要显示在界面上。

// 视口高
tableContainerHeight = 实际高(tableContentHeight) + Header高度

// 视口宽
tableContainerWidth = 实际宽(tableContentWidth) + otherWidth

2.2.2 实际宽高

// 实际高
tableContentHeight = 每行高度(rowSize) * 行数

// 实际宽
tableContentWidth = Headers配置中每行的width总和 + otherWidth

3.1 宽度计算详解

宽度适配是NewTable的一种无感(相对于组件使用者)操作宽的一种行为,也就是说,与之相关的代码逻辑是utils中的

widthToNumber -> getHeadersAndTotalWidth -> buildHeader

3.1.1 widthToNumber

宽度计算的第一步,就是需要把组件使用者给定的Headers的配置中,每一个headerItem的宽度都变为数字。widthToNumber就是负责这一步的。

无论给定的是像素(100px)还是百分比(20%),最终都会通过计算变为数字。

3.1.2 getHeadersAndTotalWidth

在第一步的基础上每一个headerItem都有宽度且宽都是数值类型(number)。

所以第二步就是把所有的宽相加求出总宽度

totalWidth === tableContentWidth

因为这一步需要遍历遍历Headers配置,所以顺便把不需要参与适配的宽度总和(notAdaptWidth)也计算出来。顺便找到左右固定列的index值。

3.1.3 buildHeader

这一步是在第一、二步的基础上,生成真正用于绘制NewTable的Headers配置,把所需要的数据都返回给NewTable

NewTable的自适应计算也是在buildHeader中去进行的。接下来在下图中详细说明一下NewTable的自适应计算方法。

3.2 tableAdaptMode === ‘auto’

auto表示NewTable自适应模式横向不会出现滚动条,宽度会随着视口进行缩放,以达到总宽度刚好等于tableContainerWidth的效果。

这个模式下自适应的思想就是,tableContainerWidth去掉不需要参与自适应的宽之后,将剩余宽度按照原有列宽的比例进行分配

不需要参与自适应的宽包含 = 配置过notAutoAdapt的列宽 + otherWidth

参与自适应的宽 = totalHeaderWidth - notAdaptWidth

需要分配给其他列的总宽度 = tableContainerWidth - (totalWidth + otherWidth)

需要分配的宽度正负都有可能

   item当前宽度            分配到的列宽(x)
------------------  =  -------------------
  参与自适应的宽度         需要分配的列宽总和


自适应后的列宽 = item当前宽度 + 分配到的列宽(可正可负)

3.3 tableAdaptMode === ‘scroll’

四、NewTable滚动条

引用之前虚拟滚动条博客中的图片和说明了。

4.1 构成

可以看到滚动条是由 3 部分组成的

4.2 计算逻辑

首先要算出滚动条dragger的高度

                  视口高度(dragger活动区域的高度)^2 
draggerHeight = ----------------------------------
                             实际高度

然后需要计算出,滚动NewTable中的内容时,滚动条需要移动的偏移量(topOffset)

注意⚠️,这里是实际高度的topOffset去影响滚动条的offset

             x                  内容滚动偏移量(topOffset)
--------------------------- = ---------------------------
  视口高度(dragger活动区高度)           内容实际高度

dragOffset = currentPage.y - startPoint.y
            x                      dragOffset
------------------------- = -------------------------
        内容实际高度           视口高度(dragger活动区高度)

五、NewTable的基础渲染

NewTable的每一个单元格都是通过绝对定位(position: absolute)绘制的,所以需要知道具体的横纵定位数值(top\ left)。

5.1 横向

横向的计算其实就是单元格left属性值的计算,用了一个map的数据结构,来存储该单元格距离NewTable左侧边(left: 0)的偏移量。其实就是该单元格之前所有单元格(不包括该单元格)宽的总和。这个map被叫做leftOffsetMap

5.2 纵向

如果是固定行高,则每一行距顶部的偏移量就是当前行index * 行高

topOffset = rowIndex * rowHeight

5.2.2 自适应高度

如果是自适应高度的行高,那么如同横向的方法一样,需要计算出每一行每一个单元格的高度找出最高的单元格高度作为这一行的行高,然后记录每一行距离NewTable顶边(top: 0)的偏移量,也就是当前行之前所有行的行高总和

六、NewTable的resize

NewTable的resize功能计算相对简单

如图中所示,首先在componentDidMount中记录初始化渲染的NewTable的宽高(realContainerWidth / Height)。

然后在resize事件中计算出窗口缩放的宽高(width/heightOffset)是多少。

tempContainerHeight = realContainerHeight - heightOffset

tempContainerWidth = realContainerWidth - widthOffset
tableContainerHeight = Max(tempContainerHright, minHeight)

tableContainerWidth = Max(tempContainerWidth, minWidth)

七、NewTable的列宽拖拽计算

列宽拖拽的计算方式依赖于NewTable的自适应方式。

因为要保证NewTable在拖拽列宽后内容始终能撑满整tableContainerWidth,所以在不同的自适应显示方法下,拖拽列宽的计算方式也不同。

7.1 tableAdaptMode === ‘auto’

当自适应模式为auto时,说明NewTable时永远都不能出现横向滚动条的,也就是说NewTable内容的宽度不能大于tableContainerWidth

在适配计算好宽度之后,拖拽列宽并不能改变NewTable的宽度总和,总和一定还是原来的值。那么就只能一列变宽多少,另一列就要变窄多少

7.2 tableAdaptMode === ‘scroll’

在适配模式为’scroll’时,NewTable内容实际的宽是有可能大于tableContainerWidth的。

这个时候如果是将一列变宽,那么就单纯的增加这一列的列宽即可,NewTable实际的宽度也会增加相同的偏移量。

如果是让一列变窄,就需要判断改变后的列宽总和是否依然大于tableContainerWidth

如果依然大于,则减少相应的宽度即可,如果小于tableContainerWidth,则应该只能让该列减少到列宽总和恰好等于tableContainerWidth的宽度。

八、NewTable的虚拟化

NewTable的虚拟化分为横向纵向两种计算方式。

虚拟化的计算没有特殊的逻辑,就是单纯的计算出能够填充满视口的数据量然后进行截取即可。

所以虚拟化的核心就是找到截取的两个端点(数组下标)即可。

8.1 横向虚拟

横向虚拟要面对的一个问题就是列宽没有规律,并非等宽,所以这个时候就要借助leftOffsetMap(记录某单元格之前所有单元格(不包括该单元格)宽的总和的map)的帮助。

const leftOffsetMap = {
  0: 0,
  1: 100,
  2: 180,
  3: 250,
  4: 298,
  5: 336,
  6: 360,
  7: 390,
  8: 430,
}

横向的虚拟主要是取决于横向滚动距离(scrollLeft)的数值。

leftOffsetMap所有小于或等于scrollLeft的值中找到最大的那一个值(leftOffsetMap中最后一个小于或等于scrollLeft的值)所对应的下标。

endIndex的寻找方式和startIndex一样

只是比较的对象从scrollLeft变成了scrollLeft + tableContainerWidth

最终找到了startIndexendIndex就可以去截取rows中的数据,然后,还给了适当的buffer(就是让截取的范围更宽些)防止出现一些极端情况。

8.2 纵向虚拟

纵向的虚拟相对于横向来说在计算上会简单一些,因为只考虑等高的行高这种情况(自适应行高应该也有办法虚拟)。

纵向的虚拟和scrollTop的距离有关。

其实startIndex计算的就是滚动滚出去多少行

startIndex = scrollTop / rowSize

// scrollTop是170. 行高是52
startIndex = Math.floor(170 / 52) = 3

然后计算视口能放下多少行

visualCount = tableContainerHeight / rowSize

// 视口高度(tableContainerHeight) 为 360
visualCount = Math.ceil(360 / 52) = 7

最后计算endIndex

endIndex = startIndex + visualCount

// 图中
endIndex = 3 + 7 = 10

九、NewTable的表头合并

NewTable的表头合并没有任何复杂的计算,只是涉及到一个递归操作。

表头部分的渲染如下图9.1所示,和一般表头渲染公用一套逻辑,只是一般表头的渲染在第一次递归就返回了。

TableRow的渲染没有给出图,一般的RowCell是一个普通的div,而表头合并的RowCell渲染,可以分成两部分看待

图中是还原有3层合并表头的布局。相同颜色代表一个层级

但基本的布局就是,上半块是父表头、下半块是子表头。以此类推的递归下去,绘制出了合并表头。

当然合并表头的实际绘制并不是真的绘制这么多层,只是为了表达一种层级关系,但其实只有一层,不是层层贴上去的。