浅析 Flex 布局中的数学原理
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第6篇文章,点击查看活动详情
现在的前端,基本上是 flex 布局的天下了,其用法简单、功能强大,是 CSS 中的一大杀器。flex 布局之所以傲视群雄,离不开其背后用到的数学原理,今天就带领大家深入了解 flex 布局中最核心的两个属性:
- flex-grow
- flex-shrink
备注:本文会用到大量小学数学知识,并配以精美的表格和插图讲述计算过程,建议阅读时间为15分钟。
flex-grow
这个属性的作用是:父元素在主轴方向还有剩余空间时,子元素们如何分配这些剩余空间。其值为一个权重,默认为 0,剩余空间将会按照各个子元素的权重来分配。假设三个子元素的 flex-grow 分别为 a,b,c,如果容器存在剩余空间 K,就会按照权重比例分配给对应的子元素,即分别增加 a*K/(a+b+c)
, b*K/(a+b+c)
, c*K/(a+b+c)
。
常规场景
举个例子:父元素宽度 500px,a、b、c 三个子元素的宽度分别为 100px,150px,100px,于是剩余空间为 150px。如果三个元素的 flex-grow 分别是 1,2,3,那么每个元素的最终宽度分别是多少呢?
按照上面的公式,由于 1+2+3 = 6,因此各元素的增长率分别是:
1 / 6
2 / 6
3 / 6
三个元素被分到的空间和最终宽度分别是:
- 增长
150px * 1 / 6 = 25px
,最终宽度100px + 25px = 125px
- 增长
150px * 2 / 6 = 50px
,最终宽度150px + 50px = 200px
- 增长
150px * 3 / 6 = 75px
,最终宽度100px + 75px = 175px
记录成表格如下:
500px 容器 | 初始宽度 | flex-grow值 | 增长率 | 增长宽度 | 最终宽度 |
---|---|---|---|---|---|
a(紫色) | 100px | 1 | 1 / 6 | 25px | 125px |
b(绿色) | 150px | 2 | 2 / 6 | 50px | 200px |
c(蓝色) | 100px | 3 | 3 / 6 | 75px | 175px |
实际效果如下:
被 max-width 干扰的场景
注意:flex-grow 会受到 max-width 的影响。如果某个元素 grow 后的结果大于 max-width 指定的值,那么 max-width 的值将会优先使用,那么剩下的元素如何分配剩余空间呢?我们把中间绿色的元素设置 max-width: 150px
让其不超过 150px,请问左边紫色和右边蓝色的元素的宽度分别是多少?
你可能会认为,既然总宽500px,中间绿色元素为150px,那么左右两个元素按照 flex-grow 的比例瓜分剩余的 350px 即可 ,即紫色元素 100px + 350px / 4 = 187.5px
,蓝色元素 100px + 3 * 350px /4 = 362.5px
。
然而并非如此,正确的计算方式是依然先按照 1:2:3 进行分配,计算得到中间的绿色元素为 200px,但是由于 max-width 为 150px,多出了 50px,那么这 50px 被左右两边的紫色和蓝色元素按照 1:3 的比例瓜分掉,也就是紫色元素宽度为 125px + 50px/4 = 137.5px
,蓝色元素宽度为175px + 3*50px/4 = 212.5px
,如下图所示:
值为小数的场景
有一点需要注意:flex-grow 取值可以是小数,例如 0.1,当所有元素的 flex-grow 之和小于 1 的时候,剩余空间不会全部分配给各个元素。实际上用来分配的空间是 sum * 剩余空间
,但比例保持不变。还是上面一个例子,但是三个元素的 flex-grow 分别是 0.1,0.2,0.3,那么计算公式将变成下面这样:
150 * 0.1 / 1 = 15px
150 * 0.2 / 1 = 30px
150 * 0.3 / 1 = 45px
三个元素的最终宽度分别为:
100px + 15px = 115px
150px + 30px = 180px
100px + 45px = 145px
即还有 150px * 0.4 = 60px
没有分配给任何子元素,如下图所示:
如果这个时候,我给中间的元素设置 max-width 为 150px 话,会是什么样的效果呢?多出来的 30px 会被左右的元素瓜分掉吗?如果是,那么分别瓜分多少呢?
其实,这种情况下剩余空间并不会分配给其余的元素,而是累加到留白的区域,即 60px 剩余空间增大到 90px。
内容自适应的场景
你可能会问,如果我们事先并没有给 a、b、c 三个子元素设置宽度的话,如何计算剩余空间呢?答案是:会根据其内容占用的宽度来计算。例如我们把三个 div 的 width 属性去掉,在里面分别添加 5 个汉字、3 个汉字和 8 个汉字,然后打开控制台通过 computed 面板来查看默认宽度。
发现一个汉字占据16px,所以a、b、c三个子元素的宽度分别是 80px、48px 和 128px,假设如果我们把容器改为 flex 布局,同样按照 1:2:3 的比例分配剩余空间,那么计算结果如下:
初始宽度 | flex-grow值 | 被分配到的剩余空间 | 最终宽度 | |
---|---|---|---|---|
a(紫色) | 80px | 1 | 40.66666px | 120.66666px |
b(绿色) | 48px | 2 | 81.33333px | 129.33333px |
c(蓝色) | 128px | 3 | 122px | 250px |
经过实践验证,也确实是如此:
总结
对于使用 flex-grow 进行从左到右布局的场景,总结如下:
- 如果所有元素的 flex-grow 之和大于等于 1,则子元素的宽度之和等于父元素宽度
- 如果 flex-grow 之和小于 1,则子元素的宽度之和小于父元素宽度
- 子元素增长率的计算公式为:元素的 flex-grow 值除以各元素 flex-grow 值之和
- max-width 可能会对 flex-grow 产生影响,影响的宽度由剩余元素按照比例重新计算
flex-shrink
该属性与 flex-grow
的作用恰恰相反,flex-shrink
用于定义:父元素在主轴方向空间不足时,子元素们如何收缩。其默认值为 1,表示元素宽度变小的一个权重分量,但是每个元素具体收缩多少,不仅仅取决于这个权重,还取决于自身的宽度。
常规场景
举个例子:父元素 400px,三个子元素宽度分别为为 150px、200px、150px,flex-shrink 的值分别为 1,2,3。通过计算得知400px - (150px + 200px + 150px) = -100px
,说明容器空间不足,还缺少 100px
,所以只能让内部的三个元素分别收缩一定的量来挤一挤,那分别收缩多少呢?
第一直觉就是:按照 1:2:3 的比例分配不就好了,各元素分别收缩 100px /6
、2*100px / 6
和 3*100px / 6
即可。
这种思路在 flex-grow 场景下是可行的,但是在 flex-shrink 下则不然,因为需要把元素自身的宽度也考虑进去,公式为:
- 总收缩宽度 = 各元素的宽度 * flex-shrink 值之和
- 各元素收缩率 = 元素宽度 * flex-shrink / 总收缩宽度
因此上面的总收缩宽度为 1 * 150px + 2 * 200px + 3 * 150px = 1000px
,各元素的收缩率分别是:
1 * 150px / 1000px = 0.15
2 * 200px / 1000px = 0.4
3 * 150px / 1000px = 0.45
因此三个元素分别收缩长度和最终宽度分别是:
- 收缩
100px * 0.15 = 15px
,最终宽度150px - 15px = 135px
- 收缩
100px * 0.4 = 40px
,最终宽度200px - 40px = 160px
- 收缩
100px * 0.45 = 45px
,最终宽度150px - 45px = 105px
记录成表格如下:
400px 容器 | 初始宽度 | flex-shrink值 | 收缩率 | 需收缩的空间 | 最终宽度 |
---|---|---|---|---|---|
a(紫色) | 150px | 1 | 0.15 | 15px | 135px |
b(绿色) | 200px | 2 | 0.4 | 40px | 160px |
c(蓝色) | 150px | 3 | 0.45 | 45px | 105px |
图示如下:
被 min-width 干扰的场景
与 max-width 会影响 flex-grow 类似,flex-shrink 也会受到 min-width 的影响,例如我们对中间绿色元素添加 min-width: 180px
的属性,左右紫色和蓝色元素最终的宽度是多少呢?
既然紫色区域锁定了 140px,不参与宽度收缩,那是不是超出的 100px 只能由左右两个元素来按照比例进行分配了呢?有了 flex-grow 的参照,相信大家也知道这么算是不对的,应该还是先按照不考虑 min-width 属性时进行计算,然后再把 min-width 超出的部分让剩余的元素按照比例承担,由于紫色的元素超出了 5px,按照上面的计算公式:
- 剩余元素加权宽度之和:
200px * 2 + 150px * 3 = 850px
- 绿色元素缩短宽度
200px * 2 * 5px / 850px = 2.35px
,因此剩余160px - 2.35px = 157.65px
- 蓝色元素缩短
150px * 3 * 5px / 850px = 2.65px
,因此剩余105px - 2.65px = 102.35px
图示如下:
值为小数的场景
同样,当所有元素的 flex-shrink 之和小于 1 时,计算方式也会有所不同,因为此时并不会收缩所有的空间,而只会收缩 flex-shrink 之和相对于 1 的比例的空间。还是上面的例子,但是 flex-shrink 分别改为 0.1,0.2,0.3,于是总权重为 150px * 0.1 + 200px * 0.2 + 150px * 0.3 = 100
。
三个元素收缩总和并不是 150px,而是只会收缩 100px 的 0.1 + 0.2 + 0.3 = 60%
的空间,即 60px,因此每个元素收缩的空间为:
60px * 0.1 * 150 / 100 = 9px
60px * 0.2 * 200 / 100 = 24px
60px * 0.3 * 150 / 100 = 27px
三个元素的最终宽度分别为:
150px - 9px = 141px
200px - 24px = 176px
150px - 27px = 123px
如下图所示:
内容自适应的场景
如果元素的宽度未指定,也会按照里面内容的大小进行动态计算,例如分别给三个元素里面添加文本,每个汉字的宽度是 16px,那么每个元素内容的宽度分别是 240px、160px 和 80px,加权宽度之和为 1 * 240px + 2 * 160px + 3 * 80px = 800px
:
表格计算结果如下:
400px 容器 | 初始宽度 | flex-shrink值 | 收缩率 | 收缩宽度 | 最终宽度 |
---|---|---|---|---|---|
a(紫色) | 240px | 1 | 0.3 | 24px | 216px |
b(绿色) | 160px | 2 | 0.2 | 32px | 128px |
c(蓝色) | 80px | 3 | 0.3 | 24px | 56px |
实际验证也确实如此:
总结
对于使用 flex-shrink 进行从左到右布局的场景,总结如下:
- 如果所有元素的 flex-shrink 之和大于等于 1,则子元素的宽度和等于父元素宽度
- 如果 flex-shrink 之和小于 1,则子元素的宽度和大于父元素宽度
- 子元素收缩率的计算公式为:flex-shrink 的值乘以元素宽度除以各元素 flex-shrink 值乘以元素宽度后的值之和
- min-width 可能会对 flex-shrink 产生影响,影响的宽度由剩余元素按照比例重新计算
转载自:https://juejin.cn/post/7142877905234690062