深度解读“布局算法”

一个可以让 CSS(层叠样式表)更直观的思维模式转变

几年前,我好像突然对 CSS 开了窍。

之前我学习 CSS 时,一直把注意力放在属性和值上(比如 z-index:10justify-content: center),认为如果可以理解每个属性的作用,就一定可以理解整个编程语言。

现在我发现 CSS 不仅是属性的集合,还是一个由紧密相连的布局算法组成的整体。每个算法都是一个复杂的系统,有专属的规则和秘密机制。

只知道每个属性的功能是不够的,还得知道布局算法的运行方式,以及布局算法如何使用我们提供的属性。

你是否有过这样的沮丧经历:你在项目中写了之前用过很多次的 CSS 块,但得到的结果却跟预想的完全不一样,从此之后你觉得编程语言非常不靠谱。

为什么输入相同的 CSS 会得到不同的输出呢?

因为这些属性被应用在一个复杂的系统上,轻微的上下文变动就会影响属性的行为。另外,心智模型不完整也是导致意外情况出现的原因之一。

研究了布局算法之后,困扰我多年的一些疑问迎刃而解,我终于意识到了 CSS 功能之强大,开始真正喜欢上使用它。

本文我们会从一个新的角度来了解 CSS 的运行方式,解决大家经常遇到的问题。:male_detective:

布局算法

什么是 “布局算法”?你可能对下列布局算法很熟悉:

  • 弹性盒子布局(Flexbox)

  • 定位布局(Positioned,例如:position: absolute

  • 网格布局(Grid)

  • 表格布局(Table)

  • 流式布局(Flow)

(严格来说,它们是布局模式,不是布局算法。但我觉得“布局算法”这个标签更有用)。

HTML 在浏览器中渲染时,每个元素都主要用一种布局算法来计算布局。我们可以根据具体的 CSS 声明来选择不同的布局算法。例如,应用 position:absolute,就切换为定位布局。

我们来看一个例子,假设我有下面的 CSS:

.box {
  z-index: 10;
}

我们的首要任务是搞清楚将会用哪种布局算法来渲染 .box 元素。依据给定的 CSS,将使用流式布局渲染 .box 元素。

流式布局是一种传统的布局算法。流式布局出现的年代里,网络还被大家当做一个巨大的超链接文件集,或是全球最大的资料库。流式布局跟 Microsoft Word 等文字处理软件使用的布局算法类似。

除非另有明确要求,非表格 HTML 元素默认使用流式布局算法。

z-index 特性可以确定堆叠顺序,如果发生元素重叠,z-index 可以确定哪个元素在“上面 ”。但是,z-index 特性无法在流式布局使用 。流式布局主要用于创建文档格式类布局,而目前的文字处理软件的元素都不可以重叠。

如果几年前你问我这个问题,我会说:

z-index 特性只能用于 position 布局,只有把 position 设置为“相对定位”或“绝对定位”才能使用 z-index。

这个回答也没错,只是有些歧义 。更准确的说法是,Z-index 特性不用于流式布局算法,如果我们想让这个特性发挥作用,需要用别的布局算法。

你可能觉得我在咬文嚼字,但是小歧义有可能会产生大影响。例如:


以上为 demo 截图,详见原文

在上面的 demo 中,我们用弹性盒子布局算法布局了 3 个元素。

中间的元素设置了 z-index 特性,确实有效 ,但移除这个元素,该元素就被置于其他元素之后。

为什么呢?因为我们没有设置 position: relative!

上文有效是因为弹性盒子算法执行了 z-index 特性。开发人员在设计弹性盒子算法时,把 z-index 特性和弹性盒子布局算法结合起来,用来控制层叠顺序,这与 z-index 特性在定位布局中的功能一样。

这是关键的心智模式转变 。CSS 特性本身没有意义,要靠布局算法来定义其功能和使用方法。

注意:

  • 部分 CSS 特性在所有布局算法中都可以使用,比如:在任一布局算法中使用 color: red 特性都会产生红色的文本。

  • 所有布局算法都可以覆盖特性的默认行为。

  • 许多特性没有默认行为。

另外,宽度特性会随着布局算法的变化而变化

下面的 demo 可以证明:


以上为 demo 截图,详见原文

我们的 .item 元素有一个单一的 CSS 特性: width: 2000px

在第一个例子中,我们用流式布局渲染.item ,这个布局会消耗 2000px 宽度。流式布局的宽度是硬性规定,会占用 2000px 的空间。

在第二个例子中,我们用弹性盒子布局渲染.item ,弹性盒子算法只有建议宽度。

弹性盒子规范称建议宽度为假设尺寸 。意思是元素在理想情况下(即没有任何约束或力量作用于它的情况下)的尺寸。这个项目的理想宽度应该是 2000px ,但该项目被放置在一个较窄的容器里,需要缩小尺寸来适应容器。

再强调一下框架的重要性。并不是说弹性盒子格局的 width 需要特别注意,只是弹性盒子格局算法实现 width 属性的方式与流式格局算法不同。*

我们编写的特性是输入 ,类似于向函数传递的参数。而如何处理这些输入,由布局算法来选择。 如果你想要了解 CSS,就需要布局算法的运行方式,只知道 CSS 特性是远远不够的。

识别布局算法

CSS 并没有 layout-mode 特性。我们可以使用一些特性来调整布局算法,但操作起来并没有那么简单。

在某些情况下,应用于元素的 CSS 特性会自行选择具体的布局模式。比如说:

.help-widget {
  /* Uses Positioned layout, because of this declaration: */
  position: fixed;
  right: 0;
  bottom: 0;
}
.floated {
  /* Uses Float layout, because of this declaration: */
  float: left;
  margin-right: 32px;
}

在其他情况下,需要查看一下父元素应用的是哪些 CSS。比如说:

<style>
  .row {
    display: flex;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

我们应用 display: flex 时,并没有对.row 元素使用弹性盒子布局算法。实际上,它的子元素应使用弹性盒子布局来定位。

严格来说,display: flex 创建了一个弹性格式化上下文。所有的直接子元素都在这个上下文中,意味着所有的直接子元素都会使用弹性盒子布局,不使用默认的流式布局。

display: flex 也会把行内元素(比如<span> )变成块级元素,所以它确实会对父元素的布局产生影响,但不会改变当前的布局算法。

布局算法的变体

一些布局算法被分割成多个变体

例如,我们使用定位布局时,可以使用下列几个不同的“定位方案”:

  • 相对定位

  • 绝对定位

  • 固定定位

  • 粘性定位

尽管这些变体确实有共同点(例如,都可以使用 z-index 属性),每个变体都有点像一个迷你布局算法。

同样地,流式布局中的元素可以是块状的,也可以是行内的。下文会细说流式布局~

矛盾

如果把多种布局算法应用于一个元素,会出现什么情况?

比如说:

<style>
  .row {
    display: flex;
  }
  .primary.item {
    position: absolute;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="primary item"></li>
  <li class="item"></li>
</ul>

这三个列表项都是弹性容器中的子元素,理应按照弹性盒子布局算法来定位。但中间的子元素设置了 position: absolute,选择了定位布局。

我对此的理解是,每个元素主要使用一种布局模式进行渲染,有点像“特异性”:有的布局模式比其他布局模式享有更高的优先权。

我不清楚确切的层次结构,但定位布局往往享有最大的优先权。因此,这个例子中的中间子元素使用定位布局,而不是弹性盒子布局。

因此,弹性盒子布局的计算就只包括两个子元素。对弹性盒子布局算法来说,不存在中间元素。这对算法没有任何影响。

一般来说,冲突都会很明显,而且是有意为之。但是,如果你发现一个元素的行为与你所设想的不一样,可以试着查看一下这个元素使用的布局算法是啥,有可能会有意外的发现。

相对定位

有一个难题是,如果每个元素都使用单一的布局算法来渲染,如何解释相对定位?

使用 Position: relative 的元素显然使用的是定位布局。这个元素可以只使用定位布局特性,如 topleft,也可以使用弹性盒子/网格布局!

我们跑题到越来越复杂的问题啦。我提供一个简单的解释,供大家参考:

每个元素都在一个特定的格式化上下文 中呈现,由布局算法来决定是否应用这个上下文。通常情况下,“定位”布局算法会忽略这些上下文,但相对定位是一个例外。

当一个相对定位的元素在弹性盒子上下文中渲染时,定位布局算法会允许该元素应用该上下文。一旦该元素使用这个上下文建立尺寸/位置,它就会应用定位布局的设置(例如,用 topleft调整位置)。

这与构图 类似。定位布局算法为相对定位的元素构成了弹性布局算法。

行内空间

我们来看一个典型的“令人困惑的 CSS”问题,看布局算法如何帮我们解决这个问题。

下面是一张内容为“一篮子猫”的图片:

大家可能会问:为什么图片下方有留白?

如果你用开发工具打开,就会发现这个图片有像素差异:

图片的高度是 250px,容器的高度却有 258.5px!

如果大家熟悉盒子模型,就一定知道元素可以使用间隙属性(padding)、边框属性(border )和 边距属性(margin )来进行间隔。大家可能会猜想:可能因为图片有边距属性,或容器有间隙属性?

实际上,与这些特性无关 。这种情况并不是常见问题导致的,我一直把这种问题称作 “魔幻的行内空间”。

要理解这个问题,我们必须对流式布局有更深入的了解。

流式布局

如上所述,流式布局是为文件设计的,就像一个文字处理软件。

文件结构如下:

  • 单个字符组合成单词和句子。这些元素排成一行,互相紧邻,如果单行的横向空间不足,会自动换行。
  • 段落被视为 ,与标题或图像一样。块呈垂直堆叠状态,一个块置于另一个块之上,从上到下。

流式布局以这个结构为基础,把单个元素处理为行内元素(并排,就像段落中的单词) 或块级元素(由上往下堆积的大块)。

42

41

语言不同,方向不同

在这个例子中,我们是以英语为例,英语由左到右横向排列的语言,但不是所有语言都是这样排列的。

有些语言(如阿拉伯语和希伯来语)是由右到左的横向书写。另外,汉文化圈内的语言(如汉语、日语和韩语)的传统熟悉方式是是由上至下垂直书写。

现在,CSS 的编写都会考虑到这些差异性。例如,我们可以写 margin-inline-start 来实现英语中的左侧边距和阿拉伯语中的右侧边距。

大多数 HTML 元素都有合理默认值。 默认<p><h1> 为块级元素,<span><strong> 为行内元素。

行内元素用于段落内,不属于布局。例如,可以在句中添加小图标。

为确保行内元素不会影响周围文本的可读性,会添加额外的垂直空间

那么,回到上文我们的疑问:为什么那个图片有多余的像素空间?因为那个图片被默认为行内元素!

流式布局算法把这个图片当作段落中的一个字符,并在图片下方添加了垂直空间,避免图片过于靠近(理论上)下一行的文本字符。

行内元素默认依据“基线”对齐。这意味着图片底部与文本的隐形水平线对齐。这就是图片下方有多余空间的原因——这些空间是为下划线准备的,比如字母 jp

因此,不是间隙属性、边框属性或边距属性,是流式布局应用于行内元素产生的行内空间。

怎么解决这个问题?

有很多方法可以解决这个问题,最简单的也许是在流式布局中把这个图片当作一个块:

行内空间对我影响很大,所以我把这个修复方法纳入了我的自定义 CSS Reset 中。

另外,只有使用流式布局才会出现这种情况,所以我们可以使用其他布局算法:

<style>
  /*
    We flip its *parent* to Flex, so
    that the child will use Flexbox
    instead of Flow:
  */
  .photo-wrapper {
    display: flex;
  }
</style>

<div class="photo-wrapper">
  <img
    class="cat-photo"
    alt="A basketful of cats"
    src="/images/cats.jpg"
  />
</div>

62

还可以使用 line-height 将多余空间缩小到 0 来解决这个问题:

71

这个方案把多余空间设置为 0 来删除留白。这样做可能会导致多行文本无法阅读,但例子中这个容器不包含文本,这个方案是可行的。

我个人更建议使用前两个解决方案。我提出第三个方案纯粹是觉得好玩(也因为第三个方案可以证明这个照片的问题是行距造成的!)。

行高和可访问性

说到行高,大家应该知道,“没有样式”的 HTML 是不可访问的,因为行距太近。如果行距不够,阅读障碍者就无法阅读文本。

大部分浏览器的默认行高为 1.1 至 1.2,但根据 WCAG 指南,正文的行高至少得设置为 1.5。

欢迎大家查看我的自定义 CSS 重置,了解我对这个问题的解决方案!

建立直觉

重点来啦:如果你只关注具体的 CSS 特性的功能,你就不会理解为什么会出现这个空间。 MDN 的页面中并没有关于 displayline-height 的解释。

如上所述,“魔法行内空间”其实根本不是什么魔法,只是流式布局算法的一条规则的副产品,即行内元素应受到 line-height 的影响。但我这么多年来一直都觉得它很神奇,其实只是因为我的心智模型不完整。

CSS 有很多布局算法,它们都有各自的特性和隐藏机制,特性只是 CSS 的冰山一角。我们需要知道那些真正重要的概念,如堆叠上下文、包含块、级联起源等。

网上的许多 CSS 教学也非常浅显。经常有人写文章或发推,分享好用的 CSS 片段,但没人能说清楚为什么那样做有效果,也没有人讲解布局算法是怎么使用 CSS 的。*

CSS 是一种的难以捉摸的调试语言,没有错误提示,也没有 debuggerconsole.log ,只能凭直觉。如果没有真正理解 CSS 就开始使用 CSS 片段,早晚会被布局算法的隐藏特性带进坑里。

我在几年前逐渐有了一些 CSS 直觉。每当遇到问题时,我会全身心地沉浸其中,深入研究 MDN 文档和 CSSWG 规范,修补代码,直到找到问题的根源。

花费这番功夫绝对值得,只是实在是太耗时了。:sweat_smile:

为了帮助开发人员节省宝贵的时间,我做了一个全面的在线课程,名为《面向 JavaScript 开发者人员的 CSS 课程》。

我会在这个课程里讲解 CSS 的运行逻辑。另外,课程的重点是为大家提供一个强大的心智模型,它可以帮大家逐步建立 CSS 直觉。我不敢保证大家再也不会再遇到 CSS 问题,但这个课程可以帮大家建立一个解决问题的工具包。

目前已有 9500 多名开发人员参加了该课程,分别来自脸书、谷歌、微软、 网飞等公司。

原文作者:Joshua Comeau
原文链接:Understanding Layout Algorithms

推荐阅读
相关专栏
开发者实践
186 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。