等宽列背后的表格布局算法

引子

网页开发中,经常需要实现等宽列效果,比如 Web App 中的 TabBar、Slider 底部的等宽指示条,而且往往元素的个数是不确定的,无法设定固定的百分比宽度。

虽然借助 flexbox 可以轻松实现:

.container {
  display: flex;
}

.item {
  flex: 1;
}

但是,你别忘了国情,我朝一些倔强的 UA 并不支持 flexbox,得找祖(qí )传(qiǎo)秘(yín)方(jì)来治这些奇葩。

基于表格布局的实现

好吧,来窥探一下 GitHub 的实现方式。

这个项目状态 GitHub 用户再熟悉不过了,简化后的 HTML 如下:

<ul class="numbers-summary">  
  <li>1,224 commits</li>
  <li>3 branches</li>
  <li>19 releases</li>
  <li>21 contributors</li>
</ul>  

实现等分效果的是下面几行样式:

.numbers-summary {
  display: table;
  table-layout: fixed;
  width: 100%;
}

.numbers-summary li {
  display: table-cell;
}  

不信?你可以试试看

其实,之前还有一个更简洁的版本

.ew-v1 li {
  display: table-cell;
  width: 1%;
}

是不是有点意思?当年被 DIV + CSS 口诛笔伐的 table 还有这玩法?而 GitHub 实现方式的细微改变又基于何种考虑呢?

其实很多人未曾真正懂 table,我也算一个。

背后的表格布局算法

带着几分迷惑,Google 一下,首先看到的当然是 GitHub 的例子,背后的原理则在 W3C 规范中有详细描述

怪我咯?

当年 table 布局被摈弃,除了代码臃肿、维护不便外,还有一个主要的原因是表格渲染时导致页面重绘带来的性能问题。

怪我咯!<table> 哥有点委屈:浏览器让我这么干的,却让我背黑锅。性能低下的根源在于浏览器默认的 table 宽度布局算法。

表格宽度算法

相信很多前端开发者都未留意过 table-layout 这个 CSS 属性,说来惭愧,我也是现在才注意到。

根据 MDN 上的文档

table-layout 属性定义 table 用于布局单元格、行、列的算法。

table-layout: auto;  
table-layout: fixed;  

autofixed 两个值分别对应 Automatic table layoutFixed table layout 两种布局算法。大多数浏览器的初始值为 auto,即默认使用自动布局算法。

Automatic table layout

自动表格布局算法中,表格及单元格宽度由其包含的内容决定,要在整个表格后加载解析完成之后才能最终确定,如果某行的列宽和前面的不一致,则之前绘制好的行也必须重新绘制,因此很低效。

列宽计算流程如下:

  1. 计算每个单元格(cell)的「最小内容宽度」(minimum content width, MCW):格式化的内容可以跨越任意行数,但不可以溢出单元格盒子。如果单元格设定的 width (W) 大于 MCW,则单元格最小宽度为 W;若 widthauto,则意味着单元格最小宽度为 MCW 。另外,计算每个单元格的最大宽度:不换行地格式化内容,明确有换行符的除外。
  2. 对于每一列(column),从仅跨越该列的单元格确定确定最大和最小宽度。最小宽度为 单元格中「最小单元格宽度」最大者及 column width 值取其大者。最大宽度为单元格中「最大单元格宽度」最大者及 column width 值取其大者。
  3. 对于跨度超过一列的单元格,增大其跨越的列的最小宽度,使得合计起来至少和单元格一样宽。最大宽度同理。如果可能,使用大概一致的值扩展合并的列。
  4. 对于每个 width 值为非 auto<colgroup>,增大贯穿其中的列的最小宽度,使使得合计起来至少和 <colgroup> 一样宽。

以此获得每一列的最大、最小宽度。

将表格的每个标题(caption)按 display: block 格式化后,放入假想的单元格得到的 MCW,就是改标题的「最小外部宽度」,其最大者即为「最小标题宽度」(CAPMIN)。

(未完待翻……)

呃!实在翻不下去连篇累牍的规范了,直接看重点吧:

A percentage value for a column width is relative to the table width. If the table has 'width: auto', a percentage represents a constraint on the column's width, which a UA should try to satisfy. (Obviously, this is not always possible: if the column's width is '110%', the constraint cannot be satisfied.)

.ew-v1 li {
  display: table-cell;
  width: 1%;
}

通过将元素设置为 display: table-cell 正是利用了自动布局算法,给 table-cell 元素设置的百分比宽度很小,但浏览器渲染的时需要确保 table-cell 元素填满表格宽度(和包含块的宽度一致),因此单元格被尽可能地扩展。

因为设置的百分比数值一样table-cell 元素渲染出来近似等宽(实际上并不相等)。假如某个元素设置的百分比更大,那渲染出来也会占据更大的空间。而且,table-cell 元素包含内容的长度也会影响宽度的分配。

宽度设置为 1% ,主要是方便添加更多元素后仍能实现预期效果。

实际上,元素容器是否设置 display: table 无关紧要,因为 table-cell 元素外围会生成一个匿名表格容器,保证正确的渲染结果。

Fixed table layout

固定表格布局算法,表格和列宽度由 <table><col> 元素的设定的宽度或者是第一行的单元格宽度决定。后面的单元格不影响列宽度。

应用此算法后,一旦表格的第一行下载、解析完成,整个表格就可以被渲染,相对于自动布局算法,可以提高渲染速度。后面的单元格内容适应方式可以通过 overflow 属性设置。

In the fixed table layout algorithm, the width of each column is determined as follows:

  1. A column element with a value other than 'auto' for the 'width' property sets the width for that column.
  2. Otherwise, a cell in the first row with a value other than 'auto' for the 'width' property determines the width for that column. If the cell spans more than one column, the width is divided over the columns.
  3. Any remaining columns equally divide the remaining horizontal table space (minus borders or cell spacing).

上述第 3 点道出了下面的 CSS 实现等分效果的玄机。

.numbers-summary {
  display: table; /* 1 */
  table-layout: fixed; /* 2 */
  width: 100%; /* 3 */
}

.numbers-summary li {
  display: table-cell; /* 4 */
}  
  1. table 的形式渲染;
  2. 使用固定表格布局算法:使未设置宽度的单元格平均分配水平空间;
  3. 保证表格填满容器(否则,表格宽度只渲染为内容宽度之和);
  4. 使子元素以表格单元格的形式渲染。

至此,已经揭开了两种实现方式背后的原理。

二者差异及兼容性

两种实现方式可能存在略微的性能差异,但几乎可以忽略,主要的差异在渲染结果上

  • 自动布局算法:没有完全等分,而且受单元格内容长度影响,即便长度一致,差异也很大;
  • 固定布局算法:完全等分(由于浏览器计算原因会有 1px 差异),不受内容长度影响

所以,选择哪种方式就显而易见了。

浏览器兼容:

  • Can I Use 数据 :IE8+ 及其他浏览器都支持 display: table-* 属性;
  • table-layout: IE5 等古董浏览器都支持。

所以,放心使用吧。

写在最后

Web 开发中若遇到迷惑的问题,试着翻翻规范,说不定你就提壶光腚、茅厕蹲开了。

通过组合一些看似不起眼甚至枯燥乏味的属性,实现「神奇」的效果,正式 CSS 的精髓和乐趣所在。

参考链接

咦!竟然有这「算法」这么高大上的东东。科班大神们就别欺负半路出家的前端、拿算法虐我们了……Amen.