详解 flex-grow 与 flex-shrink

自从开始开学习 CSS 布局,想要比较灵活的把父元素的空间分配给各个子元一直是各个前端程序员的梦想。

在 flex 之前,如果不是专门去搜索相关的解决方案,一般人几乎想不出非常灵活的三(多)栏等高布局方案,而即使看了解决方案,很多人也会大呼奇技淫巧。

不得不感慨在 flex 之前 CSS 的布局功能之弱:基本只能使用一些并非为布局而设计的属性来实现想要的布局——float、inline-block、position、甚至是 table 等。而使用这些属性来实现各种布局效果,往往又会遇到相当多另外的坑:比如浮动的闭合、inline-block 的垂直对齐、position 的定位原点以及 table 的不够灵活等。

直到出现了 flex

flex 可以说是一次性解决了前端布局的所有的问题(当然,并没有完全解决,要不然也不会有 grid layout 了),以前很难实现的布局效果在 flex 下简直不能更简单,以至于一些其它平台也开始吸纳 flex 的布局思想,也有些开源项目把 flex 的布局方式移植到其它平台。

中文社区也有不少写 flex 的文章,比如 ruanyifeng。然而个人觉得不少写 flex 的文章都有个通病,就是一上来就整一堆 flex 相关的术语,比如 flex container,flex item,main axis(主轴),cors axis(交叉轴),不禁让人望而生畏,都还没搞清楚怎么回事,就来一堆术语。

然而这还不是最大的问题,最大的问题是很多文章并没有把 flex 布局的详细计算方式讲清楚,尤其是连 ruanyifeng 的文章也没把这事说清楚,但是在 Google 搜索 flex 相关的文章,他的文章却会出现在第一页。因为我觉得他写的并不好,所以就不贴地址了,想看的同学可以自己搜一下,就在第一页。

即使是 MDN 以及《The Book Of CSS3》里也没把 flex-grow 和 flex-shrink 的计算方式说清楚。

所以我决定写这一篇文章,把 flex-grow 与 flex-shrink 的详细计算方式讲清楚。

flex 如何解决传统常见布局问题

在传统布局中最常见也是急需的当然就是在从左往右把父元素的空间分配给子元素以实现多栏布局了:按比例也好,定宽也好,更灵活的定宽加占用剩余空间也好。

那我们就从使用 flex 如何实现三栏布局开始吧。

想要实现三栏等高布局,且两边的侧栏宽度固定而中间一栏占用剩余的空间,如下代码就足够了:


<style>
  section {display: flex;}
  .left-side,
  .right-side {width: 200px;}
  .content {flex-grow: 1;}
</style>
<section>
  <div class="left-side"></div>
  <div class="content"></div>
  <div class="right-side"></div>
</section>

其中 section 元素的宽度将会像 block 元素一样尽量的宽,对外面的元素来说,它的行为很像一个 block 块。三个元素会从左往右占据父元素的空间(这很显然)。左右侧边栏的宽度都是 200px,中间 .content 元素的宽度将会占据 section 元素的剩余宽度。

另外,section 的高度会自动被最高的一个子元素撑开,同时其它子元素的高度也会被拉到跟 section 元素一样高,而如果给 section 元素设置了高度,而所有子元素的高度设置为 auto ,所有的子元素也都会自动跟父元素一样高,这简直就是在传统布局中做梦都想要的功能!

总之,在高度方面,flex 的表现是相当符合直觉的。

另外,如果不给 flex 子元素设置宽度和 flex-grow,它会尽量的窄。

flex-grow 的计算方式

上面 demo 中最值得注意的是 .content 元素的 flex-grow 属性,设置为 1 它就可以占满水平剩余空间。这也是本文的重点:讲清 flex-grow 与 flex-shrink 属性的详细计算方式。

flex-grow 属性决定了父元素在空间分配方向上还有剩余空间时,如何分配这些剩余空间。其值为一个权重(也称扩张因子),默认为 0(纯数值,无单位),剩余空间将会按照这个权重来分配。

比如剩余空间为 x,三个元素的 flex-grow 分别为 a,b,c。设 sum 为 a + b + c。那么三个元素将得到剩余空间分别是 x * a / sum, x * b / sum, x * c / sum,是为权重也。

举个例子:

父元素宽度 500px,三个子元素的 width 分别为 100px,150px,100px。

于是剩余空间为 150px

三个元素的 flex-grow 分别是 1,2,3,于是 sum 为 6
则三个元素所得到的多余空间分别是:

  • 150 * 1 / 6 = 25px
  • 150 * 2 / 6 = 50px
  • 150 * 3 / 6 = 75px

三个元素最终的宽度分别为 125px,200px,175px。

  • 100px + 25px = 125px
  • 150px + 50px = 200px
  • 100px + 75px = 175px

可以打开这个 demo(下文中所有的 demo 都在这个页面) 然后用开发工具查看一下。注意不要用截图工具量,可能量不准,因为高分屏和放大等诸多因素都会影响测量结果。

然而!不止这些,还有一种情况:

当所有元素的 flex-grow 之和小于 1 的时候(注意是 1,也就是说每个元素的 flex-grow 都是一个小数如 0.2 这样的),上面式子中的 sum 将会使用 1 来参与计算,而不论它们的和是多少。也就是说,当所有的元素的 flex-grow 之和小于 1 的时候,剩余空间不会全部分配给各个元素。

实际上用来分配的空间是 sum(flex-grow) / 1 * 剩余空间,这些用来分配的空间依然是按 flex-grow 的比例来分配。

还是上面一个例子,但是三个元素的 flex-grow 分别是 0.1,0.2,0.3,那么计算公式将变成下面这样:

  • 150 * 0.1 / 1 = 15px
  • 150 * 0.2 / 1 = 30px
  • 150 * 0.3 / 1 = 45px

150px - 15px - 30px - 45px = 60px,即还有 60px 没有分配给任何子元素。
三个元素的最终宽度分别为:

  • 100px + 15px = 115px
  • 150px + 30px = 180px
  • 100px + 45px = 145px

如上所述即是 flex-grow 的计算方式。

另外,flex-grow 还会受到 max-width 的影响。如果最终 grow 后的结果大于 max-width 指定的值,max-width 的值将会优先使用。同样会导致父元素有部分剩余空间没有分配。

flex-shrink 的计算方式

前文已经说到,flex 几乎一次性解决了前端布局的所有问题。

那么既然可以在空间有多余时把多余空间分配给各个子元素,当然也可以在空间不够时让各个子元素收缩以适应有限的空间了。

这就是 flex-shrink 属性的作用。

你可能会觉得 flex-shrink 的计算方式跟 flex-grow 很类似,然而事情并没有这么简单。

flex-shrink 属性定义空间不够时各个元素如何收缩。其值默认为 1。很多文章对此基本是一笔带过:“flex-shrink 属性定义了元素的收缩系数”,根本就不说它具体是怎么计算的。

flex-shrink 定义的仅仅只是元素宽度变小的一个权重分量。

每个元素具体收缩多少,还有另一个重要因素,即它本身的宽度。

举个例子:

父元素 500px。三个子元素分别设置为 150px,200px,300px。

三个子元素的 flex-shrink 的值分别为 1,2,3。

首先,计算子元素溢出多少:150 + 200 + 300 - 500 = -150px。

那这 -150px 将由三个元素的分别收缩一定的量来弥补。

具体的计算方式为:每个元素收缩的权重为其 flex-shrink 乘以其宽度。

所以总权重为 1 * 150 + 2 * 200 + 3 * 300 = 1450

三个元素分别收缩:

  • 150 * 1(flex-shrink) * 150(width) / 1450 = -15.5
  • 150 * 2(flex-shrink) * 200(width) / 1450 = -41.4
  • 150 * 3(flex-shrink) * 300(width) / 1450 = -93.1

三个元素的最终宽度分别为:

  • 150 - 15.5 = 134.5
  • 200 - 41.4 = 158.6
  • 300 - 93.1 = 206.9

同样,当所有元素的 flex-shrink 之和小于 1 时,计算方式也会有所不同:

此时,并不会收缩所有的空间,而只会收缩 flex-shrink 之和相对于 1 的比例的空间。

还是上面的例子,但是 flex-shrink 分别改为 0.1,0.2,0.3。

于是总权重为 145(正好缩小 10 倍,略去计算公式)。

三个元素收缩总和并不是 150px,而是只会收缩 150px 的 (0.1 + 0.2 + 0.3) / 1 即 60% 的空间:90px。

每个元素收缩的空间为:

  • 90 * 0.1(flex-shrink) * 150(width) / 145 = 9.31
  • 90 * 0.2(flex-shrink) * 200(width) / 145 = 24.83
  • 90 * 0.3(flex-shrink) * 300(width) / 145 = 55.86

三个元素的最终宽度分别为:

  • 150 - 9.31 = 140.69
  • 200 - 24.83 = 175.17
  • 300 - 55.86 = 244.14

当然,类似 flex-grow,flex-shrink 也会受到 min-width 的影响。

总结

虽然上面的公式看起来很复杂,其实计算过程还是比较简单的:如果所有元素的 flex-grow/shrink 之和大于等于 1,则所有子元素的尺寸一定会被调整到适应父元素的尺寸(在不考虑 max/min-width/height 的前提下),而如果 flex-grow/shrink 之和小于 1,则只会 grow 或 shrink 所有元素 flex-grow/shrink 之和相对于 1 的比例。grow 时的每个元素的权重即为元素的 flex-grow 的值;shrink 时每个元素的权重则为元素 flex-shrink 乘以 width 后的值。

相关 w3 文档地址:CSS Flexible Box Layout Module Level 1

结语

本来是想写一篇事无巨细关于 flex 的文章,没想到刚刚写完 flex-grow/shrink 的计算方式就花了这么大的篇幅。考虑到 flex 其它方面的文章已经有很多人写了,各位可以看看 ruanyifeng 的相关文章,毕竟配图还是相当好的。

也可以在下面这个页面中看到 flex 实时布局的效果:Flexbox Playground

或者玩一下下面这个以 flex 为背景的游戏:Flexbox Froggy

如果你还觉得意犹未尽,可以看看我的下面几篇博文:

关于 flex 的其它相关问题

下面列一下在 flex 布局中容易忽略的几个问题:

  • flex 上下文中垂直 margin 不会合并
  • flex 主轴方面上子元素的 margin 如果设置为 auto,其该方向上的 margin 是会尽量大的,可以利用这个特性来做对齐
  • flex-basis 与 width/height 同时使用时的优先级问题

最后,广告时间

我叫谢然,网名充电大喵,高二开始接触编程,2011 年毕业于华中师范大学计算机科学与技术专业,曾先后就职于全国百强中学、阿里巴巴、小米。2016 年 9 月创建了自己的前端培训品牌 “大喵教育前端培训”,首期前端培训课程已过半。第二期班将于 2017 年 3 月上旬开班,地点杭州下沙。

我的培训班与众不同,着重培养学生的自学能力,英语能力(部分课程使用英文授课),及计算机基础能力(包括算法与数据结构及计算机网络),让非科班的人也能有机会进入 IT 行业,我为学员设计了与众不同的且有难度的实践项目:

  • 初步实现 lodash
  • 初步实现 JSON Parser
  • 初步实现 jQuery
  • 初步实现 Sizzle
  • ……

如果你想参加,或者想要了解更多,请点击:大喵教育前端培训

以上。

编辑于 2016-12-14 22:18