了解布局算法

185 阅读16分钟

几年前,我在学习CSS时有了一个*"尤里卡*"时刻。

直到那一刻,我一直在学习CSS,把注意力集中在我们写的属性和值上,比如z-index: 10justify-content: center 。我想,如果我大致了解每个属性的作用,我就会对整个语言有深刻的理解。

我的关键认识是,CSS不仅仅一个属性的集合。它是一个由相互连接的布局算法组成的星座。每个算法都是一个复杂的系统,有自己的规则和秘密机制。

仅仅学习特定属性的作用是不够的。我们需要学习布局算法如何工作,以及它们如何使用我们提供给它们的属性。

你是否有过这样令人不安的经历:写了一大块熟悉的CSS,你以前用过很多次,却得到一个不同的、意想不到的结果?这是很令人沮丧的。这让人感觉到语言的不一致性和不稳定。完全相同的CSS输入怎么会产生不同的输出呢?

发生这种情况是因为属性作用于一个复杂的系统,而且有一些微妙的背景改变了属性的行为方式。我们的心智模型是不完整的,这导致了意外的发生!

当我开始钻研布局算法时,一切都开始变得更有意义。困扰了我多年的谜团被解开了。我意识到,CSS实际上是一种非常强大的语言,我开始真正喜欢上了写它

在这篇博文中,我们将看看这个新镜头如何帮助我们理解CSS中发生的事情。我们将用这个镜头来解开一个令人惊讶的谜题。🕵️

[

链接到这个标题

](www.joshwcomeau.com/#layout-alg…

那么,什么是 "布局算法"?你可能已经熟悉了其中的一些。它们包括

  • Flexbox

  • 定位(例如:position: absolute

  • 网格

  • 表格

(从技术上讲,它们被称为布局模式,而不是布局算法。但我觉得 "布局算法 "是一个更有用的标签)。

当浏览器渲染我们的HTML时,每个元素的布局都会用一种主要的布局算法来计算。我们可以通过特定的CSS声明来选择不同的布局算法。例如,应用position: absolute ,可以将一个元素转换为使用定位布局。

让我们看一个例子。假设我有以下的CSS。

css

我们的首要任务是弄清楚哪种布局算法将被用于渲染.box 元素。根据所提供的CSS,它将使用Flow布局进行渲染。

Flow是网络上的 "OG "布局算法。它创建于一个时代,当时网络主要被视为一个巨大的超链接文档集,就像世界上最大的档案馆。它类似于微软Word等文字处理软件中使用的布局算法。

Flow是用于非表格HTML元素的默认布局算法。除非我们明确选择其他布局算法,否则将使用Flow。

z-index 属性是用来控制堆叠顺序的,如果它们重叠,就会计算出哪一个显示在 "上面"。但问题是:**它没有在Flow布局中实现。**Flow是关于创建文档式布局的,而我还没有看到允许元素重叠的文字处理软件。*

如果你在几年前问我这个问题,我一定会说这样的话。

你不能使用z-index ,而不同时将position 设置为 "相对 "或 "绝对 "之类,因为z-index 属性取决于position 属性。

**这并不完全是错误的,但这是一个微妙的误解。**更准确的说法是,z-index 属性没有在 Flow 布局算法中实现,因此,如果我们想让这个属性产生影响,就需要选择一个不同的布局算法。

这可能看起来像我在迂腐,但这种小的误解会导致大的混乱。例如,考虑一下这个。

Code Playground

使用Prettier格式化代码

重置代码

HTMLCSS

<style>

结果

刷新结果窗格

启用 "tab "键

在这个演示中,我们有3个兄弟姐妹使用Flexbox布局算法排列。

中间的兄弟姐妹设置了z-index ,**而且还能工作。**试着去掉它,发现它落在了它的兄弟姐妹后面。

*这怎么可能呢?*我们并没有在任何地方设置position: relative!

这是因为Flexbox的算法实现了z-index 属性。当语言作者在设计Flexbox算法时,他们决定将z-index 属性连接起来,以控制堆叠顺序,就像它在Positioned布局中所做的那样。

**这就是关键的思维模式转变。**CSS属性本身是没有意义的。要由布局算法来定义它们的作用,以及它们在计算中的使用方式。

明确地说,有一些CSS属性在所有布局算法中的作用是相同的。color: red ,无论如何都会产生红色文本。但每种布局算法都可以覆盖任何属性的默认行为。而且许多属性没有任何默认行为。

**这里有一个让我大吃一惊的例子。**你知道吗,width 属性的实现方式因布局算法的不同而不同?

这里就是证明。

Code Playground

使用Prettier格式化代码

重置代码

HTMLCSS

<style>

结果

刷新结果窗格

启用 "tab "键

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

.item 的第一个实例是用Flow布局渲染的,它实际上会消耗2000px的宽度。在Flow布局中,宽度是一个硬性规定。它将占用2000px的空间,后果不堪设想。

然而,.item第二个实例是在一个Flex容器内呈现的,这意味着它使用了Flexbox布局。在Flexbox算法中,宽度更像是一种建议。

Flexbox规范称其为假设的尺寸。这是该元素在田园诗般的世界里,在没有任何约束或力量作用于它的情况下会有的尺寸。在一个完美的世界里,这个项目应该是2000px宽,但它被放在一个较窄的容器里,所以它会缩小以适应它。

再一次,框架在这里是超级重要的。这并不是说width ,在涉及到Flexbox时有什么特殊的注意事项。而是Flexbox算法以不同于Flow算法的方式实现了width 属性。*

**我们写的属性是输入,**就像传递给函数的参数。由布局算法来选择如何处理这些输入。如果我们想理解CSS,我们需要理解布局算法是如何工作的。仅仅知道这些属性是不够的。

[

链接到这个标题

](www.joshwcomeau.com/#identifyin…

CSS并没有一个layout-mode 的属性。有几个属性可以调整所使用的布局算法,实际上它可以变得非常棘手

在某些情况下,应用于一个元素的CSS属性会选择一个特定的布局模式。比如说。

css

在其他情况下,我们需要看一下元素应用了什么CSS。比如说。

html

当我们应用display: flex ,我们实际上并没有为.row 元素使用Flexbox布局算法;相反,我们是说它的子元素应该使用Flexbox布局来定位。

从技术上讲,display: flex ,创建了一个Flex格式化的上下文。所有直接的子元素都将参与这个上下文,这意味着它们将使用Flexbox布局而不是默认的Flow布局。

(display: flex 也会把一个内联元素,如<span> 变成一个块级元素,所以它确实对父元素的布局有一些影响。但它不会改变使用的布局算法)。

[

链接到这个标题

](www.joshwcomeau.com/#layout-alg…

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

例如,当我们使用Positioned layout时,就是指几种不同的 "定位方案"。

  • 相对式

  • 绝对定位

  • 固定式

  • 粘性

每个变体都有点像它自己的迷你布局算法,尽管它们确实有共同之处(例如,它们都可以使用z-index 属性)。

同样地,在Flow布局中,元素可以是块状的,也可以是内联的。我们很快会讨论更多关于Flow布局的问题。

[

链接到这个标题

](www.joshwcomeau.com/#conflicts)…

当多种布局算法应用于一个元素时会发生什么?

比如说。

html

所有三个列表项都是Flex容器中的子项,所以它们应该按照Flexbox来定位。但中间的子项通过设置position: absolute ,选择了定位布局。

按照我的理解,一个元素将使用一个主要的布局模式来渲染。这有点像特异性:某些布局模式比其他模式有更高的优先级。

我不知道确切的层次结构,但是Positioned布局往往会打败一切。因此,在这个例子中,中间的孩子将使用Positioned布局,而不是Flexbox。

因此,Flexbox的计算方式就好像只有两个孩子,而不是三个。就Flexbox算法而言,这个中间的孩子并不存在。它对算法完全没有影响。*

一般来说,冲突通常是很明显的/故意的。但如果你发现一个元素的行为与你所期望的不一样,那就值得尝试去确定它使用的是哪种布局算法。答案可能会让你吃惊

相对定位

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

一个带有position: relative 的元素显然是使用Positioned布局来渲染的。它可以使用专属的Positioned-layout属性,如topleft 。然而,它也可以参与Flexbox/Grid布局

我们开始偏离本文的范围了。这东西变得很复杂但这里有一个快速解释,供那些好奇的人参考。

显示更多

[

链接到这个标题

](www.joshwcomeau.com/#inline-mag…

好吧,让我们看看一个典型的 "令人困惑的CSS "问题,看看关注布局算法如何帮助我们解决这个问题。

这里我们有一篮子的猫。

Code Playground

使用Prettier格式化代码

重置代码

HTMLCSS

<div class="photo-wrapper">

结果

刷新结果窗格

启用 "tab "键

嗯......为什么图片下面有一点多余的空间?

如果你用你的开发工具检查,你会发现有几个像素的差异。

图片的高度是250px,但容器的高度是258.5px

如果你熟悉盒子模型,你就知道元素可以用padding、border和margin进行间隔。你可能认为图像上有一些边距,或者容器上有一些填充物?

在这种情况下,**这些属性都不需要负责。**这就是为什么多年来,我私下里把这种情况称为 "内联魔法空间"。它并不是由通常的罪魁祸首造成的。

为了理解这里发生的事情,我们必须对Flow布局进行更深入的挖掘。

[

链接到这个标题

](www.joshwcomeau.com/#flow-layou…

如前所述,Flow布局是为文档设计的,类似于文字处理软件。

文档有以下结构。

  • 单个字符被组合成单词和句子。这些元素是并列的,在没有足够的水平空间时可以换行。

  • 段落被认为是,就像标题或图像。块将被垂直堆叠,一个在另一个上面,从上往下。

流程布局是基于这种结构的。单个元素可以被安排为内联元素(并排的,就像段落中的单词),或作为块状元素(从上往下堆叠的大块砖)。

The same page as earlier, but with animated annotations. Labels show the block direction (vertical) and the inline direction (horizontal).

方向因语言而异

在这个例子中,我假设我们使用的是一种水平的、从左到右的语言,如英语。这并不是普遍的情况。

有些语言,如阿拉伯语和希伯来语,是从右到左的水平书写。其他以汉族为基础的语言,如汉语、日语和韩语,传统上是从上到下垂直书写的。

以一种适应这些差异的方式来写CSS已经变得很流行。例如,我们可以写margin-inline-start ,在英语中针对左侧边距,而在阿拉伯语中针对右侧边距。

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

内联元素是为了在段落中间使用,而不是作为布局的一部分。例如,也许我们想在一个句子的中间添加一个小图标。

为了确保内联元素不会对周围文本的可读性产生负面影响,会增加一些额外的垂直空间

所以,把这个问题带回我们的谜题:为什么我们的图像有几个额外的像素空间?因为图像是默认的内联元素。

福禄布局算法把这个图片当作段落中的一个字符,并在下面增加了一点空间,以确保它不会与(理论上的)下一行文本上的字符靠得很近,令人不舒服。

默认情况下,内联元素是 "基线 "对齐的。这意味着图像的底部将与文本所处的不可见的水平线对齐。这就是为什么图像下面有一些空间--这些空间是为下划线准备的,比如字母jp

因此,这不是边距,或填充,或边框......这是Flow布局应用于内联元素的一点内在空间。

[

链接到这个标题

](www.joshwcomeau.com/#solving-th…

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

代码乐园

使用Prettier格式化代码

重置代码

HTMLCSS

<style>

结果

刷新结果窗格

启用 "tab "键

在我的职业生涯中,内联魔法空间已经咬了我很多次,所以我把这个确切的修复方法纳入了我的自定义CSS Reset中。

另外,由于这种行为是Flow布局所特有的,我们可以翻转到不同的布局算法。

Code Playground

使用Prettier格式化代码

重置代码

HTMLCSS

<style>

结果

刷新结果窗格

启用 "tab "键

最后,我们也可以通过将额外的空间缩小到0来解决这个问题,使用line-height

代码游乐场

使用Prettier格式化代码

重置代码

HTMLCSS

<style>

结果

刷新结果窗格

启用 "tab "键

这个解决方案通过将行距设为0来移除所有额外的行距。这将使多行文本完全无法阅读,但由于这个容器不包含任何文本,所以这不是一个问题。

我建议使用前面两个解决方案中的一个。提出这个方案纯粹是因为它很有趣(也因为它证明了问题是由行距引起的!)。

行高和可及性

当我们在讨论行高的时候:你是否知道,"无样式 "的HTML实际上被认为是不可访问的,因为线条之间的距离太近?如果行间距不大,有阅读障碍的人就很难解析文本。

大多数浏览器的默认行高是1.1到1.2,但根据WCAG指南,我们应该把正文的行高至少设为1.5。

请看我的自定义CSS重置,看看我是如何解决这个问题的。

[

链接到这个标题

](www.joshwcomeau.com/#building-a…

**所以,重点来了:**如果你只专注于研究特定的CSS属性的作用,你永远不会明白这个神秘的空间是怎么来的。displayline-height 的MDN页面中并没有解释它。

正如我们在这篇文章中所了解到的,"内联魔法空间 "其实根本就不是什么魔法。它是由Flow布局算法中的一条规则引起的,即内联元素应该受到line-height 。但对我来说,它似乎很神奇,多年来,因为我的心理模型中有这个大洞。

CSS中有很多布局算法,它们都有各自的怪癖和隐藏机制。当我们专注于CSS属性时,我们只看到了冰山的一角。我们从来没有学习过真正重要的概念,如堆叠上下文或包含块或层叠起源

不幸的是,网上的许多CSS教学也同样浅薄。在博客文章或推特上分享一个方便的CSS片段是很常见的,但却没有解释为什么它能发挥作用,也没有解释布局算法如何使用它。*

CSS是一种难以调试的语言;我们没有错误信息,也没有debugger ,或console.log 。我们的直觉是我们拥有的最好的工具。当我们开始使用CSS片段而没有真正理解它们时,只是时间问题,直到布局算法的某些隐藏方面给我们的齿轮带来麻烦,使我们停滞不前。

几年前,我决定开始培养自己对CSS的直觉。每当我被一些出乎意料的行为所困扰时,我就会像洗热水澡一样沉浸在这个问题中。我会深入研究MDN文档和CSSWG规范,并对代码进行修补,直到我觉得我已经真正弄清楚了事情的真相。

这是一项绝对值得的投资,但我的天哪,它花了很长时间。😅

我想为其他开发者加快这个过程。我最近发布了一个全面的在线课程,叫做《JavaScript开发者的CSS》。

Banner with text “CSS for JavaScript Developers”

在这门课程中,我们探讨了CSS在引擎盖下是如何工作的。它专注于提供一个你可以使用的强大的心智模型,这个模型可以帮助你一次一次地建立起对CSS的直觉。我不能保证你永远不会再遇到CSS挑战,但我可以帮助你建立克服这些挑战所需的工具箱。

到目前为止,已经有超过9500名开发人员参加了这一课程,他们来自Facebook、谷歌、微软、Netflix等组织,还有许多其他组织。反应是压倒性的积极。

你可以在课程主页上了解更多:
css-for-js.dev。