on
NewTable技术说明
NewTable 是 Sugar Design 中的表格组件。
相关链接
一、NewTable的布局
NewTable
是被tableWrapper
所包裹的,tableWrapper
的作用就是将NewTable
与外界隔开,所有的外界样式作用于tableWrapper
。
在tableWrapper
中包含NewTable
本身(tableContainer)和两个滚动条(横向 / 纵向)。
NewTable
本身(tableContainer)包含3大块:
- TableHeader
- TableBody
- TableFooter
TableBody
的内部则是由若干TableRow
构成(图中未给出)。
还包含一些其他的元素,比如数据为空时,TableBody
和TableFooter
都会被EmptyContent
(非NewTable的配置属性)所替换。
二、NewTable关键定义
2.1 基本含义
引出Table中最重要的4个变量(也是NewTable组件的state)的含义是什么。
图中浅灰色的矩形(较小的)就是Table在页面中所显示的大小,称之为视口宽高。
而图中深灰色的内容则是Table本身数据全部展示需要的大小,称之为实际宽高。
- 视口宽 :tableContainerWidth
- 视口高:tableContainerHeight
- 实际宽:tableContentWidth
- 实际高:tableContentHeight
- 其他宽:otherWidth,指
rowSelection
的checkbox
所占宽度(固定68)以及draggable
的icon
所占宽度(固定24)
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)的值只会在两种情况下再次改变:
NewTable
开启resize配置项且发生了resize事件NewTable
传入的height / 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)
需要分配的宽度正负都有可能
-
正值说明: 给定的总宽 < tableContainerWidth需要填充的宽度以至于能撑满NewTable
-
负值说明: 给定的总宽 > tableContainerWidth每一列需要压缩宽度以至于能在NewTable中放得下。
item当前宽度 分配到的列宽(x)
------------------ = -------------------
参与自适应的宽度 需要分配的列宽总和
自适应后的列宽 = item当前宽度 + 分配到的列宽(可正可负)
3.3 tableAdaptMode === ‘scroll’
-
如果Header中
配置的列宽总和
>tableContainerWidth
,那么不需要任何的适配,按照配置的列宽绘制即可。 -
如果
totalHeaderWidth
<tableContainerWidth
-otherWidth
时,适用tableAdaptMode === ‘auto’的自适应计算方法。
四、NewTable滚动条
引用之前虚拟滚动条博客中的图片和说明了。
4.1 构成
可以看到滚动条是由 3
部分组成的
- 滚动条容器(滚动条所能显示的区域,应该与视口一样高)
- 滚动条容器的实际高度(应该与视口内容的实际高度一致)
- 滚动条的 dragger(可拖拽的 bar,本身的高度应该与视口高度和实际高度之间的比例有关)
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的值)所对应的下标。
- 最上面的图示,此时
NewTable
并没有进行横向滚动,此时scrollLeft = 0
,最后一个小于或等于scrollLeft
的值是下标0对应的值。所以此时startIndex就是0 - 中部的图示,此时发生了横向滚动,
scrollLeft = 60
,最后一个小于或等于scrollLeft
的值是下标0对应的值,所以此时startIndex依然是0 - 下方的图示,此时
scrollLeft = 150
,最后一个小于或等于scrollLeft
的值是下标1对应的值,所以此时的startIndex是1
endIndex
的寻找方式和startIndex
一样
只是比较的对象从scrollLeft
变成了scrollLeft + tableContainerWidth
最终找到了startIndex
和endIndex
就可以去截取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渲染,可以分成两部分看待
- 对外,就是把整个父表头对应的列看成一列,和普通表头一样,占用一个div(RowCell)
- 对内,这个div内部有若干子RowCell来分别渲染对应子列的单元格
图中是还原有3层合并表头的布局。相同颜色代表一个层级。
但基本的布局就是,上半块是父表头、下半块是子表头。以此类推的递归
下去,绘制出了合并表头。
当然合并表头的实际绘制并不是真的绘制这么多层,只是为了表达一种层级关系,但其实只有一层,不是层层贴上去的。