Flexbox 是一种 CSS 布局机制,可以说是目前浏览器原生支持的最好、使用最广泛的布局机制了。本文通过一些例子来说明 Flexbox 布局的工作原理,可以让我们更好的使用 Flexbox。

与 CSS Grid 可以同时在横向和纵向两个方向进行布局不同,Flexbox 只能在单一方向上进行布局,即要么横向,要么纵向。所谓布局,其实就是空间的分配过程,也就是说计算元素尺寸和容器剩余空间尺寸的过程。

Flexbox 的布局原理

整个布局过程我们可以简单的总结如下:

  1. 计算 flex 容器内的可用空间。整个容器的尺寸减去容器的 border、padding 等所得的剩余空间尺寸。
  2. 计算每个 flex 元素的 flex base 尺寸和元素的假设尺寸。具体计算方法是取 flex-basis、min-width 和 flex 元素内容尺寸的较大者。flex base 尺寸是 flex 元素需要的最小尺寸,这个尺寸不能小于元素内容的尺寸。元素的假设尺寸是指在 flex 因子生效前元素的尺寸,flex 因子生效后可能导致元素发生伸缩。
  3. 计算容器内所有 flex 元素的假设尺寸总和。
  4. 将所有元素的假设尺寸总和与容器内可用空间尺寸做比较,来确定 flex 因子,也就是说当假设尺寸总和超过容器内可用空间尺寸时,使用 flex-shrink,否则使用 flex-grow。在同一时间,flex-shrink 和 flex-grow 只有一个生效。

所谓 flex 因子,简单来说就是缩小和放大。浏览器在进行 flexbox 布局时会先确定使用哪种 flex 因子,然后再根据选用的 flex 因子来对元素尺寸进行调整。

在进行调整的时候,就会涉及到一个剩余空间的计算问题。如果 flex 元素明确指定了尺寸大小(definite sizeopen in new window,比如设置了 width),那么这个元素就是不可伸缩的。如果没有显式指定尺寸,则会按照上面第2步那样计算假设尺寸。剩余空间的尺寸就是容器内的可用空间尺寸减去这些元素的尺寸之和。

例子说明

我们有如下 dom 结构:

<div id="flex">
  <div id="a">Antidisestablishmentarianism</div>
  <div id="b">B</div>
  <div id="c">Cherries jubilee</div>
  <div id="d">D</div>
  <div id="e">E</div>
</div>
1
2
3
4
5
6
7

样式如下:

[id=flex] {
    font-weight: 300;
    display: flex;
    outline: 1px dashed #555;
    width: 1200px;
    margin: 3rem auto;
}
/*其他样式已省略*/
1
2
3
4
5
6
7
8

页面展示效果如下:

flex 布局

我们没有设置元素的 flex 样式属性,默认值是 0 1 autoflex 样式属性是 flex-growflex-shrinkflex-basis 这三个样式属性的简写形式。0 1 auto 分别对应为 flex-growflex-shrinkflex-basis 的值。

通过取值可以看到,因为我们禁止了放大和收缩,并且 flex-basis 的值是 auto,浏览器就使用元素的最大内容尺寸来计算所有元素的尺寸总和,比容器的尺寸(1200px)小,所有会有额外的剩余空间。

关于 flex 这个样式属性,我们额外做一些说明。flex 属性可以接收最少一个、最多三个属性值。

当只有一个属性值的时候,flex 的工作模式是这样的 <number> 1 0。即 flex: 2 最终的结果是 flex: 2 1 0

当有两个属性值的时候,第一个值会被解析为 flex-grow,第二个值如果是数字,则会被解析为 flex-shrink,如果是一个合法的宽度值open in new window则会被解析为 flex-basis。即 flex: 1 0 解析为 flex: 1 0 0,而 flex: 1 20rem 则被解析为 flex: 1 1 20rem

当有三个属性值的时候,第一个值被解析为 flex-grow,第二个值必须为数字,且会被解析为 flex-shrink,第三个值则必须是合法的宽度值,被解析为 flex-basis

设置 flex-grow

我们增加如下样式设置:

[id=flex] > div {
  flex: 1;
}
1
2
3

实际上等同于 flex: 1 1 0,即 flex-grow: 1。从前面的例子我们可以看到,所有元素的假设尺寸之和是小于容器剩余空间尺寸的,所以浏览器会使用 flex-grow 来作为 flex 因子,因为我们设置了 flex-grow: 1 ,因此浏览器会等比放大所有的元素。如下图:

设置了 flex-grow 的 flex 布局

实际上浏览器会循环通过下面的公式来计算每个元素的最终尺寸:

当前元素伸缩值 = (当前剩余空间 - 所有剩余元素的 flex-grow 值的和) * 当前元素的 flex-grow 值
1

在上面的例子中,容器的剩余空间为 1200px,因此,通过公式计算:

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

元素 A 的最终尺寸为 240px + flex-basis = 240px + 0 = 240px。但是,因为 Antidisestablishmentarianism 这个单词比较长,实际会占用 417px 的空间大小,因此元素 A 的最终尺寸为 417px

此时,在计算元素 B 的尺寸时,剩余空间为 1200px - 417px = 783px。元素 B 的伸缩值为:

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

同理,元素 C、D、E 的伸缩值分别为:

C: ( 587 ÷ ( 1 + 1 + 1 ) ) × 1 ) = 195.67
D: ( 391.33 ÷ ( 1 + 1 ) ) × 1 = 195.665
E: ( 391 - 195.665 ÷ 1 ) × 1 = 195.335
1
2
3

总结来说,浏览器从剩余空间中减去已经分配的空间,然后计算下一个 flex 元素的伸缩值和最终大小。

差异化的 flex-grow

我们修改一下元素的 flex-grow 值:

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

[id=c] {
  flex: 5;
}
1
2
3
4
5
6
7

此时,页面展示效果如下:

差异化 flex-grow

各元素的伸缩值计算如下:

A: 417px
B: ( 783 ÷ ( 1 + 1 + 5 + 1 ) ) × 1 = 98
C: ( 685 ÷ ( 5 + 1 + 1 ) ) × 5 = 490
D: ( 195 ÷ ( 1 + 1 ) ) × 1 = 97.5
E: ( 97.5 ÷ 1 ) × 1 = 97.5
1
2
3
4
5

设置 flex-basis: auto

前面的例子里,我们都设置了 flex-basis 的值为 0,现在我们设置 flex-basis: auto,再来看看元素尺寸是如何计算的。

删除其他元素的 flex 指定,修改样式如下:

[id=c] {
  flex: 5;
}
1
2
3

此时等价于其他元素是 flex: 0 1 auto,元素 C 是 flex: 5 1 0。展示效果如下:

flex-basis

在前面的例子中,所有元素都是 flex-basis: 0,现在设置为 A、B、D、E 都设置成了 flex-basis: auto。因此,元素 A、B、D、E 都会使用元素内容的尺寸来计算。

因此,剩余剩余可分配空间大小为:

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

因为元素 C 是唯一可伸缩的元素,因此它的伸缩值为 685。

设置 flex-shrink

现在我们做一些调整,让所有 flex 元素的尺寸总和大于容器的可用空间尺寸,使得 flex-shrink 生效。

样式调整如下:

:not([id=a]) {
    flex-shrink: 1;
}
[id=a] {
    flex-shrink: 5;
}
div > div {
    flex-basis: 500px;
}
1
2
3
4
5
6
7
8
9

可以看到,所有元素 flex-grow: 0,元素 A flex-shrink: 5,其他元素 flex-shrink: 1,所有元素总尺寸 2500px。

显示效果如下: flex-shrink

因为元素尺寸总和超过了容器可用尺寸(1200px),因此 flex-shrink 将会生效。

元素 A 的伸缩值为:

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

则元素 A 的实际尺寸计算为:

500 - 722.22 = -222.22
1

可以看到得出的是负值。如果元素 A 是空元素,那么最终尺寸会是零。本例中,元素 A 的尺寸就是起内容的尺寸,大概 34px。

确定了元素 A 的尺寸之后,容器剩余空间尺寸为 1200 - 34 = 1166。其他元素的伸缩值计算为:

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
1
2
3
4

值得注意的一点是,如果 flex-growflex-shrink 的取值都为 0,那么元素即不会放大也不会缩小,当所有元素的尺寸总和超过容器空间之后,就会产生溢出效果。

设置 flex-wrap

当所有元素的尺寸总和超过容器空间之后,就会产生溢出效果。我们可以通过设置 flex-wrap: wrap 来是超出的元素换行。

我们调整下样式:

div > div {
    flex-basis: 500px;
}
[id=flex] {
    flex-wrap: wrap;
}
1
2
3
4
5
6

显示效果如下:

flex-wrap

我们可以看到,由于 flex-basis: 500px 且默认的 flex-grow: 0,因此每一行的末尾有 200px 的剩余空间。

我们可以通过设置 flex-grow: 1 来让元素占满剩余空间。

div > div {
    flex: 1 1 500px;
}
1
2
3

此时显示效果如下:

flex-wrap with flex-grow

可以看到,每行的剩余空间都被占满了。

总结

Flexbox 布局有时候会有一些复杂和难以理解。在实际使用过程中,我们需要牢记如下几点:

  • Flexbox 只在单一方向上分配空间,行或者列。
  • flex-basis 定义了元素的最小尺寸,有时候元素的内容尺寸可能会比 flex-basis 定义的尺寸大。
  • 浏览器同时只会使用 flex-grow 或者 flex-shrink 来排列元素,不会同时使用。
  • 实际使用 flex-grow 还是 flex-shrink 取决于元素尺寸总和与容器剩余空间的大小。
  • 浏览器根据 flex 因子以及元素本身设置的系数来分配每个元素的空间。

关注微信公众号,获取最新推送~

加微信,深入交流~