从误解 flex: 1 到理解 flexbox

1,176 阅读8分钟

背景

我们经常在某些业务场景中,需要实现子元素均等占用父元素空间的效果。到目前为止,我使用最多的就是用 flex: 1 来实现。直到有一天我突然想使用 flex-grow: 1 来达到相同的效果,但是浏览器却给了我不一样的表现,我才发现我忽略了**剩余空间(free space)**这个关键词。一直以来,我是以 flex container 的宽度为基础来考虑空间分配的问题,简单得将 flex: 1flex-grow: 1 划上了等号。 本文是重新学习了 flexbox 之后做的总结,主要围绕 flexflex-basisflex-grow 以及 flex-shrink 这 4 个属性。

在 Firefox 审查 flexbox 布局的节点可以看到一些尺寸数据,但是 Chrome 好像并不能,所以文中的代码都会在 Firefox 中审查,我的 Firefox 版本是 65.0.2。

flexbox 基本概念

关于一些基础知识,其他文章已经有详细的说明,本文就只是简单表述一下。

  1. 首先,设置了 display: flex 的元素会成为一个 flex container,并创建一个 FFC(Flex Formatting Context),而该元素本身则会表现为一个块级元素,相应的,设置了 inline-flex 的元素则表现为内联元素

  2. flex container 内部的元素会成为 flex item,包括内部的纯文本(会被包裹一个匿名块级盒子)。绝对定位元素因为不会参与到 flex 布局中来,所以不会成为一个 flex item

  3. 最后,在进行 flex layout 时,flex 会生效,使 flex item 完全填充至 flex container 的可用空间,或者收缩 flex item 以阻止溢出。当所有的 flex item 确定了尺寸之后,就会根据 justify-contentalign-self 等属性进行排列和布局。

flex-basis

flex-basis 会代替 flex item 在主轴(main axis)方向上的尺寸属性(width/height),并在分配剩余空间(free space)之前初始化 flex item 的主轴尺寸。

接受的值与 width 或者 height 相同,比如数值、百分比、automin-content 以及 max-content,另外还有 content

  • auto

    被指定为 auto 时,会取当前 flex item 主轴尺寸属性(width/height)的值,如果取到的值是 auto 时,则会使用 content,也就是根据 flex item 的内容大小来确定。比如以下结构:

    <div style="display: flex; width: 200px; border: 1px dotted #333;">
      <div id="first" style="background: #eee; width: 80px; flex-basis: auto;">
        short text
      </div>
      <div id="second" style="background: #ccc; flex-basis: auto;">
        very looooooooong text
      </div>
    </div>
    

    使用 Firefox(支持 content,但是 min-contentmax-content 是无效值)审查节点的时候可以看到: 因为设置了 width: 80px,所以 div#first 的基础尺寸是 80px,而 div#second 的宽度是 auto,所以它的基础尺寸就基于它的内容得到,即内容尺寸。

  • content(兼容性不好)

    被指定为 content 时,是根据 flex item 的内容大小来确定尺寸的。相当于使用了 width: max-content。Chrome 是不支持该值的,Firefox 可以使用该值。

flex-grow

简单讲,flex-grow 是指当前 flex item 可以从 flex container 的剩余空间(positive free space)里分配到多少比例。

这里有一个关键词是positive free space,而我之前误解成了 flex container 的整个 content box

例如 flex-grow: 1 就是指希望分配到 100%positive free spaceflex-grow: 0.5 就是指 50%,以此类推。所以,当所有的 flex-grow 总和小于 1 时,这些 flex items 也就没有分配到 100% 的 positive free space,就会有空间被剩下;当所有的 flex-grow 总和大于 1 时,就会以 100% 为基础重新计算比例,毕竟不会有超过 100% 的空间可以分配。

flex-shrink

MDN 中的定义:

flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。

可以用 negative free space 来表示溢出的空间。与 flex-grow 不同的是,如果指定 flex-shrink: 1,不能简单的理解为这个 flex item 希望分配到 100% 的 negative free space。这里的 1 相当于一个权重,具体的计算过程会在如何计算弹性中描述。

在收缩规则中,有一个概念是 content-based minimum size,直译过来是基于内容的最小尺寸。它限制了 flex item 可以收缩的程度,不会让 flex item 收缩到更小的尺寸。它的值可能是一个确定的 主轴(main axis)尺寸属性值(width/height),也可能是 min-content。这个概念会在如何计算弹性中使用到。

flex

flex: 0 1 auto 是它的初始值。也就是说 flex item 在默认状态下,可以在主轴维度上缩小但不能拉伸,并且根据 width/height 来确定其大小。

而被我误解为等价于 flex-grow: 1flex: 1,完整的值是 flex: 1 1 0flex: <positive-number> 1 0)。

如何计算弹性

<div style="display: flex; width: 200px; border: 1px dotted #333;">
  <div id="first" style="background: #eee;">
    short text
  </div>
  <div id="second" style="background: #ccc; width: 280px;">
    very looooooooong text
  </div>
</div>

以上述代码为例:

  1. 确定主轴(main axis)可用空间(available space),也就是 flex containercontent box,可能是一个确定的尺寸,也可能需要计算得出。

    例子中的可用空间就是 200px。

  2. 根据 flex-basis 确定每一个 flex item基础尺寸(flex base size)

    由于 div#firstflex-basiswidth 都为 auto,所以他的基础尺寸就是它的内容尺寸,通过 Firefox 审查节点可以看到它的内容尺寸是 59.93px。而 div#second 由于设置了 width: 280px,所以 280px 就是它的基础尺寸。

    另外,在这一步骤中,最大最小约束(这个例子是 min-wdithmax-width)会被忽略。如果给 div#first 一个 min-width: 100px,它的基础尺寸依然是 59.93px。至于这些约束何时生效,可以在后面的步骤看到。

  3. **计算 flex item主轴尺寸(main size)总和,确定使用 flex-grow 还是 flex-shrink

    例子中,可以得到总和是 339.93px,超过了 flex container 的尺寸,所以可以确定 flex-shrink 生效。

  4. 确定 positive free space 或者 negative free space 的大小

    首先,需要计算例子中 flex-shrink 的总和,得到值为 2。因为大于 1,所以 negative free space 的值就是 339.93px - 200px = 139.93px。

  5. 为每个 flex item 分配 free space,确定修改后的尺寸。

    因为在这个例子中生效的是 flex-shrink 属性,所以每个 flex item 的最终大小是基础尺寸减去它所分配到的 negative free space。那么,每一个 flex item 应该被分配到多少呢? 在这个公式里,可以看到基础尺寸与 flex-shrink 的值做了相乘,也就是上文flex-shrink中说到的权重。可以简单套用这个公式:

    • div#first : (339.93 - 200) * 59.93 / (59.93 + 280) = 24.67px
    • div#second: (339.93 - 200) * 280 / (59.93 + 280) = 115.26px

    所以最后 div#first 的宽度是 35.27px,div#second 是 164.73px。
在这个例子中,以上就是最终尺寸了,可以在 Firefox 审查节点看到这个结果。

    但是,如果将 div#second 的宽度调整到 1000px,就会有接下来的这一步,我们可以看到flex-shrink中提到的 content-based minimum size 会起到限制作用,步骤 2 中的最大最小约束也会生效。

  6. 根据 min/max-width(min/max-height) 或者 content-based minimum size 修正 flex item 的尺寸。

    我们把 div#secondwidth 设置为 1000px,然后套用上面的公式:

    • div#first : (1059.93 - 200) * 59.93 / (59.93 + 1000) = 48.62px
    • div#second: (1059.93 - 200) * 1000 / (59.93 + 1000) = 811.31px

    如果没有把这一步骤写出来,也许你会认为 div#first 的最终尺寸就是 59.93 - 48.62,div#second 是 1000 - 811.31。我们可以在 Firefox 审查一下: div#first 的内容尺寸以及弹性的确和我们预想的一样,而最终尺寸却是 31.97px。在这里就是 content-based minimum size 生效了,它使用 div#firstmin-content 作为了它的值。

    如果我们设置了一个确定的 width,content-based minimum size 其实会取 widthmin-content 之间的最小值,比如 width: 10px,那么 div#first 的最终尺寸就是 10px。

    我就是希望 div#first 的最终尺寸是 59.93 - 48.62 = 11.31px,这该如何做呢?为 div#first 加上 min-width: 5px,这样一来它的最终尺寸就成了 11.31px。

如果 flex-grow 生效,如何计算

以这段代码为例:

<div style="display: flex; width: 300px; border: 1px dotted #333;">
  <div style="background: #eee; flex-grow: 2;">
    short text
  </div>
  <div style="background: #ccc; flex-grow: 1;">
    very looooooooong text
  </div>
</div>
  1. 首先,主轴的可用空间是 300px。
  2. 每一个 flex item 的基础尺寸是它们的内容尺寸,分别是 59.93px 和 152.82px。
  3. flex item 的主轴尺寸总和是 212.75px,所以 flex-grow 生效。
  4. 确定 positive free space 的大小是 87.25px。
  5. 计算 flex item 分配到的 positive free space,分别是 87.25 * 2 / (2 + 1) = 58.17px 和 87.25 * 1 / (2 + 1) = 29.08px。确定最终尺寸分别是 118.1px 和 181.9px。

最后

本文做的总结基本上可以覆盖我目前碰到的业务场景了,未来也许会碰到更多的场景来调整我对 flexbox 的理解。关于 flex layout 的算法,W3C有非常详细且复杂的介绍,本文做了一定的调整和简化。如果想要了解更多,可以去W3C看看一些介绍。