【译】Flexbox 工作原理

371 阅读12分钟

原文地址:How Flexbox Works

原作者:Tiffany B. Brown

Flexbox 弹性盒子布局和其它 CSS 部分不同,很难仅通过使用来理解。直到最近修订《CSS Master》这本书,我才算是有点搞懂了(说实话,我也不能确定我百分之百搞懂了)。本文旨在为大家揭开它的神秘面纱。

Flexbox 的设计初衷是在一个容器元素内部分配元素和空间。与 CSS Grid 不同,它只能沿着行(row)或列(column)的单一方向进行布局。

要分配空间,浏览器必须先确定有多少空间是可分配的。计算过程大致如下(更详细的 Flexbox 算法说明请查阅此规范):

  1. 计算 flex 容器内部的可用空间。可用空间尺寸等于容器的主轴尺寸(main size)减去其外边距(margin)、内边距(padding)、边框(borders)和间隙(gaps)之后的尺寸。
  2. 使用flex-basismin-width 、flex 项目(flex item)内容尺寸三者中的最大值,计算每个 flex 项目的基础尺寸(Flex base size)和假定主轴尺寸(hypothetical main size。基础尺寸是 flex 项目所需的最小尺寸。假定主轴尺寸指的是 flex 项目应用弹性因子(flex factors)之前的尺寸。flex 项目的基础尺寸永远不会小于其内容尺寸。
  3. 计算所有 flex 项目的总假定主轴尺寸
  4. 比较所有 flex 项目的总假定主轴尺寸和 flex 容器的可用空间。浏览器会使用flex-shrinkflex-grow的值计算每一行 flex 项目的尺寸。当所有 flex 项目的总假定主轴尺寸超过可用空间时,浏览器会使用flex-shrink的值作为弹性收缩因子;当总假定主轴尺寸小于可用空间时,浏览器会使用flex-grow的值作为弹性增长因子。

弹性因子决定了在剩余空间(Leftover space)中每个 flex 项目需要增加或减少的比例。浏览器会循环计算每个 flex 项目的布局尺寸。

剩余空间(Leftover space)是指从 flex 容器的内部宽度中减去所有具有确定尺寸的项目、内边距和间隙所占据的空间后,剩余下来的空间。可以将具有确定尺寸的项目称为“非弹性项目(inflexible item)”,而没有确定尺寸的项目称为“弹性项目(flexible items)”,只有弹性项目的尺寸才会根据弹性盒模型算法进行调整。这种尺寸调整的过程就叫做弹性布局。

我个人认为 Firefox 的 Flexbox 检查器目前最能清晰地解释 Flexbox 的工作原理。Chrome 和 Edge 会标识 flex 项目是否已经增长或收缩,但是 Firefox 还会告诉您增长或收缩了多少。我建议使用 Firefox 来检查本文中的示例。

在本文的所有示例中,弹性容器的宽度都为 1200 像素。

一个简单的 Flexbox 示例

图1使用了flex属性的默认值 — 0 1 autoflex属性是flex-growflex-shrinkflex-basis属性的简写。在这里,flex属性的值如下:

  • flex-grow属性值为零;
  • flex-shrink属性值为 1;
  • flex-basis属性值为auto

由于flex-basis属性值为auto,因此浏览器使用max-content的值作为每个 flex 项目的基础尺寸。这些 flex 项目的总假定主轴尺寸小于 flex 容器中可用的空间量。因此,这些 flex 项目既不会增长以填充可用空间,也不会收缩以适应其中。

图1:一个简单的 Flexbox 示例。你也可以在新 Tab 页签中查看本例

在继续之前,我们先稍微了解一下flex属性的语法

了解一下flex属性的语法

flex属性至少接受一个值,最多可接受三个值。

当仅给定一个值时,这个值被解释为flex: <number> 1 0,其中<number>flex-grow的值。换句话说,flex: 2等同于flex: 2 1 0

当给定两个值时,第一个值被解释为flex-grow的值。对于第二个值:

  • 如果第二个值是数字,则会被解释为flex-shrink属性的值。
  • 如果它是 width 属性的有效值,那么它会被解释为flex-basis的值。

换言之,flex: 1 0相当于flex: 1 0 0,但是flex: 1 30rem等价于flex: 1 1 30rem

当给定三个值时:

  • 第一个值是flex-grow值。
  • 第二个值必须是一个数值,表示flex-shrink的值。
  • 第三个值必须是width属性的有效值。它被解释为flex-basis的值。

flex-basis理解为初始尺寸。当一个 flex 项被设置为增长(set to grow)时,浏览器会给flex-basis增加弹性空间(add flexibility);当它被设置为收缩(set to shrink)时,浏览器会从这个值中减少弹性空间(subtract flexibility)。

当 flex 项目增长时

图2展示了当每个 flex 项都添加了flex:1;(相当于flex:1 1 0;)时会发生什么。

图2:将flex:1添加到每个 flex 项后。你也可以在新 Tab 页签中查看本例

在这个示例中,这些 flex 项目的总假定主轴尺寸小于 flex 容器的可用空间量。因此,浏览器使用flex-grow作为弹性增长因子。这里每个 flex 项目的flex-grow值都是1flex-basis值都是0

浏览器会循环遍历每个 flex 项目,使用以下公式来计算其弹性空间:

弹性空间 = (剩余可分配空间 ÷ 剩余弹性元素的flex-grow之和) × flex-grow

flex-basis的值加上或减去弹性空间大小就能得到元素的最终尺寸。根据这个公式,我们来计算一下 A 元素的弹性空间大小。记住,所有示例中的容器宽度都是 1200 像素。

( 1200 ÷ ( 1 + 1 + 1 + 1 + 1 ) ) × 1 = 240

元素 A 具有 240 像素的弹性空间。将这个值加到它的flex-basis值上,得到它的最终尺寸为:0 + 240 = 240。

根据这个计算结果,元素 A 的宽度应该是 240 像素。然而,Antidisestablishmentarianism 是一个没有连字符的长单词,需要占用 417 像素的空间(文本所需的空间取决于未使用连字符和不间断空格的单词长度、字体大小以及所选字体中每个字形的尺寸。在本文的所有示例中,我都使用了 IBM Plex Sans 字体以保证一致性。)。浏览器不会将 A 的大小设为 240,而是将其宽度夹紧到其最小内容大小的 417 像素。现在还有 783 像素的空间要在 B、C、D 和 E 元素中分配。

对于 B 元素,计算公式如下(记住每个弹性元素的flex-basis值都为0):

( 783 ÷ ( 1 + 1 + 1 + 1 ) ) × 1 = 195.75

0 + 195.75 = 195.75

将元素 B 的尺寸从剩余空间中减去:783-195.75=587.25。剩下大约 587 像素可以分配给元素 C、D 和 E。

C: 0 + ( 587 ÷ ( 1 + 1 + 1 ) ) × 1 ) = 195.67

D: 0 + ( 391.33 ÷ ( 1 + 1 ) ) × 1 = 195.665

E: 0 + ( 391 - 195.665 ÷ 1 ) × 1 = 195.335

浏览器从剩余空间中扣除已分配的空间,然后计算下一个 flex 项目的弹性空间大小。

一个 flex 项目需要多少空间,取决于:

  • font-size属性值;
  • 字形的宽高;
  • 特定单词和文本行的长度;
  • 以及 flex 项目内包含的元素(如图像和视频)的内在或指定大小。

不同浏览器和操作系统对文本的呈现方式也会有所影响。

flex-grow和分配比例

在图3中,元素 C 的flex值为5flex: 5)。

图3:flex-grow如何按比例分配空间。你也可以在新 Tab 页签中查看本例

元素 C 的兄弟元素有一个flex: 1的声明。以下是 CSS 代码。

div > :not([id=c]) {
  flex: 1;
}

[id=c] {
  flex: 5;
}

还记得吗?flex: 1相当于flex: 1 1 0,那么flex: 5就相当于flex: 5 1 0。在这个例子中,每个 flex 项目都被设置为从零的flex-basis值开始增长。但是,flex-grow值为5意味着 C 元素应该获得大约是其兄弟元素五倍的弹性空间。

同样地,Antidisestablishmentarianism 也强制使元素 A 具有 417 像素的内联尺寸。这样,就剩下 783 像素可以按比例分配给元素 B、C、D 和 E。

我们来计算一下元素 B 的尺寸。现在还有四个 flex 项目,其中 C 的flex-grow值为 5。这意味着剩余的 783 像素将按照每个弹性元素的flex-grow之和(也就是 8)来分配。然后将这个数值加上我们的 flex 基础尺寸:0。

0 + ( 783 ÷ ( 1 + 1 + 5 + 1 ) ) × 1 = 98

元素 B 大约有 98 像素宽。783 减去 98,剩余约 685 像素的空间。现在我们来计算元素 C、D 和 E 的尺寸。

C: 0 + ( 685 ÷ ( 5 + 1 + 1 ) ) × 5 = 490

D: 0 + ( 195 ÷ ( 1 + 1 ) ) × 1 = 97.5

E: 0 + ( 97.5 ÷ 1 ) × 1 = 97.5

由于元素 C 的flex-grow值为 5,因此它的弹性空间是元素 B、D 和 E 的五倍。

flex-basisauto

图4中,元素 C 的flex-grow值仍然是 5,其兄弟元素的flex属性设置为:0 1 auto

图4:当应用flex-basis: auto时,flex-grow如何工作。你也可以在新 Tab 页签中查看本例

在上面的示例中,元素 A、B、D 和 E 的flex-grow值为0。由于它们的flex-basis值为auto,它们的内联尺寸将与其内容所需的宽度一样。对于元素 A,它的尺寸是“Antidisestablishmentarianism”和粗体字母 A 的尺寸之和。对于元素 B、D 和 E,它们的尺寸就是每个字母的大小。

元素 A、B、D、E 都有明确的大小。我们可以立即确定容器中的剩余空间:

1200 - ( 417 + 33 + 35 + 30 ) = 685

由于 C 是容器中唯一的可伸缩项目,它将增长以填充剩余空间。

0 + 685 = 685

再次声明,这些计算结果我都做了四舍五入。在你的浏览器中审查元素可能会看到略有不同的结果。

当 flex 项目收缩时

接下来我们来看一个使用flex-shrink作为弹性收缩因子的 Flexbox 布局示例。在图5中,每个 flex 项目的flex-basis值均为 500 像素。元素 B、C、D、E 的flex-shrink值为 1,flex-grow值为 0。元素 A 的flex-shrink值为 5。

图5:当给定一个大于 1 的flex-shrink时。你也可以在新 Tab 页签中查看本例

在这个示例中,所有 flex 项目的flex-basis值之和为 2500 像素,这是它们的总假定主轴尺寸。由于总假定主轴尺寸大于容器的尺寸,浏览器会使用flex-shrink作为弹性收缩因子。

我们先来计算一下元素 A 的弹性空间。首先计算 flex 项目的假定尺寸与 flex 容器尺寸之间的差值。

( 1300 ÷ ( 5 + 1 + 1 + 1 + 1 ) ) × 5 = 722.22

接下来,从flex-basis当中减去这个值。

500 - 722.22 = -222.22

得到的结果是一个负数。如果 A 是一个空元素,那么它的主轴尺寸将被固定为 0 像素。如果不是,浏览器会将其尺寸夹紧到最小内容尺寸——也就是 34 像素左右。

既然 A 的尺寸我们已经知道了,那么就可以从容器中减去这个尺寸:

1200 - 34 = 1166

现在我们就能够计算出每个 flex 项目的尺寸了。

B: ( 1166 ÷ ( 1 + 1 + 1 + 1 ) ) × 1 = 291.5

C: ( 874.5 ÷ ( 1 + 1 + 1 ) ) × 1 = 291.5

D: ( 583 ÷ ( 1 + 1 ) ) × 1 = 291.5

E: ( 291.5 ÷ 1 ) × 1 = 291.5

需要注意的是:当flex-growflex-shrink的属性值都为 0 时,flex 项目既不会增长也不会收缩。如果所有 flex 项目的总假定主轴尺寸超过弹性容器的主尺寸, flex 项目将沿主轴堆叠直至溢出容器。可以通过将flex-wrap的值设置为wrapwrap-reverse来更改此行为。

接下来看一下浏览器在处理多行的情况下是如何确定弹性空间的。

Flexbox 和flex-wrap

图6中,我在图5的基础上,将flex-wrap的值从默认的nowarp更改为wrapflex属性值仍和图5中保持一致。

图6:添加flex-wrap: wrap后,总假定主轴尺寸超过弹性容器尺寸时会自动换行。你也可以在新 Tab 页签中查看本例

flex-wrap属性值为wrapwrap-reverse时,浏览器会使用每个 flex 项目的flex-basis值来确定每行可以容纳多少项目。当 flex 项目的假定主轴尺寸之和超过容器尺寸时就会在该点换行。

在图6中,flex-grow的属性值为 0。因此,在前两行末尾会有 200 像素的剩余空间。相反,将flex-grow值更改为 1 就会产生图7中所示的结果。每个 flex 项目会自动增长以填充其所在行的剩余空间。

图7:当 flex 容器应用flex-wrap: wrap属性且flex-grow大于零时,flex 项目会自动扩展以填充其所在行的剩余空间。你也可以在新 Tab 页签中查看本例

flex-wrap属性值设置为wrap-reverse也是类似的。不同的是,wrap-reverse还会翻转 flex 项目的可视化渲染顺序。

总结

Flexbox 和其它 CSS 领域不同,很难通过使用来理解。需要注意以下几个要点:

  • Flexbox 在单个方向上(沿着行或列)分配空间。
  • flex-basis属性值确定了 flex 项目的初始尺寸或者说基本尺寸。但是,flex 项目内容的固有尺寸可以强制使 flex 项目比flex-basis值更大。
  • 对于 flex 项目的任意行,浏览器只会应用flex-growflex-shrink中的一个属性,而不会同时应用这两个属性。
  • 浏览器使用flex-grow还是flex-shrink作为弹性因子(flex factor)取决于在主轴方向上有多少可用空间。
  • 可用空间会按照各个 flex 项目的弹性因子比例进行分配。