上一篇结尾留了一条线索:flex 项目在主轴方向默认不长大,空间不够时又会被压缩。这两个默认行为来自三个属性——flex-grow、flex-shrink、flex-basis,而它们平时几乎都是以 flex 简写的形式出现的。这一篇就按它们实际参与计算的顺序,把“一项最终有多宽”这件事讲清楚。
第一步:先定“基准尺寸”,这由 flex-basis 负责
伸缩发生之前,每一项在主轴方向上要先有一个起始大小,这个起始大小叫 flex basis(基准尺寸),由 flex-basis 指定。
它的初始值是 auto,含义是“去问主轴方向的尺寸属性要答案”:flex-direction: row 时看 width,column 时看 height。也就是说:
- 写了
width: 200px,基准尺寸就是 200px; width也是auto,就看内容本身——更准确地说,是内容在不需要换行时要占的宽度(max-content)。
flex-basis 一旦写成具体长度(如 flex-basis: 100px)或百分比,它就 压过 width。同时写了 width: 200px 和 flex-basis: 100px,起算点是 100px,不是 200px。百分比相对于容器内容盒的主轴尺寸 3。
可以把两者的分工记成一句:width 是常规流里的尺寸,flex-basis 是“伸缩开始之前你在主轴上的起点”。对 flex item 来说,起点由 flex-basis 决定 23。
第二步:把基准尺寸加起来,算出富余空间
所有项的基准尺寸相加,再和 Flex 容器主轴方向可用的空间比一比,差出来的那部分就叫 富余空间(free space) 1:
- 结果为正:容器比内容宽,叫正富余空间,交给
flex-grow处理; - 结果为负:内容比容器宽,叫负富余空间,交给
flex-shrink处理。
这一步能解释一个常见困惑:flex-grow 在空间刚好或者不够时完全没有表现,flex-shrink 在空间够用时也完全没有表现。它们各自只负责一半情况。这跟上一篇 justify-content 在没有富余空间时几个值看起来一样,是同一类原因。
flex-grow:分的是“多出来的那部分”,而且只是比例
flex-grow 的初始值是 0,意思是“有富余也别给我”。设成正数,它才参与分配。
关键是:flex-grow 的数值表示的 不是最终宽度的比例,而是富余空间的分法。看一个具体例子:容器主轴 600px,三个盒子 width 都是 100px,flex-grow 分别是 1、2、3。
- 基准尺寸之和:px;
- 富余空间:px;
- grow 因子之和:,所以每份是 px;
- 各自拿到:、、,即 50px、100px、150px。
最终宽度是 150px、200px、250px,比值是 3:4:5,不是 1:2:3。grow 只影响“多出来的 300px 怎么分”,原本的 100px 谁也不动 4。
这正好解释了很多人第一次用它的困惑:三个宽度不同的盒子都写 flex-grow: 1,富余空间是平分的,但每一项是在自己的原始宽度上加上同样一份,所以结果仍然不等宽。如果希望它们最终完全一样宽,就不能让“原始宽度”参与计算——那是下面要讲的 flex-basis: 0。
flex-shrink:不够的空间按“基准尺寸的比例”扣
flex-shrink 的初始值是 1,这就是“主轴方向空间不够时会被压缩”的来源。
它的算法和 grow 有个重要差别:每个项的收缩因子会 先乘上自己的基准尺寸,得到一个缩放后的因子,再按这个因子去分摊负富余空间 24。原因是让大块头多出一点血,小块头不至于先被压没。
容器 600px,三个盒子 flex-basis 分别是 400px、200px、300px,flex-shrink 都是 1:
- 基准尺寸之和 900px,负富余空间 300px;
- 缩放后的因子:、、,总和 900;
- 各自被扣掉 、、。
结果是 266.7px、133.3px、200px,加起来正好 600px。顺带能看出:收缩因子相同时,每一项都按同一个比例缩小(这里都变成原来的 ),所以它们之间的宽度比例被保留了下来。
如果某一项写 flex-shrink: 0,它完全不缩,缺额只能由其他项分担;如果所有项都不缩,容器就溢出。
还有个更隐蔽的下限:flex item 默认 不会缩到比自己内容的最小尺寸(min-content)还小。一段长长的、中间不能断行的文字,会把它的最小尺寸撑得很大,于是它拒绝继续缩,容器被撑破或者出现横向滚动条。想让文字换行、继续缩下去,需要给这一项加上 min-width: 0(column 方向则是 min-height: 0)15。这是实际写页面时最常见的 flex 触发点之一。
flex 简写:省略的项用的是简写自己的默认值
flex 是 flex-grow、flex-shrink、flex-basis 的简写。它的坑不在语法,而在 省略时的默认值:简写里省略的 grow 和 shrink 分别按 1 和 1 处理,省略的 basis 按 0% 处理——这不是三个长写属性的初始值(0、1、auto)25。
| 写法 | 展开为 | 效果 |
|---|---|---|
flex: 1 | 1 1 0% | 基准按 0 算,整个容器空间按 grow 比例分,三栏等宽 |
flex: 2 2 | 2 2 0% | 第二个值是 shrink,basis 被一起改成 0% |
flex: 1 30px | 1 1 30px | 第二个值是长度,就当 basis 用 |
flex: auto | 1 1 auto | 基准来自 width 或内容,富余空间仍会吸收 |
flex: initial | 0 1 auto | 不占富余空间,但不够时会缩(默认值) |
flex: none | 0 0 auto | 完全不伸缩,宽度按 width 或内容 |
由此能看清两组最容易被混淆的写法。
flex: 1 和 flex: auto 不是一回事。 把三个宽度分别是 100px、200px、50px 的盒子放进 600px 的容器:
- 都写
flex: 1(即1 1 0%):基准全部归零,600px 全是富余空间,平分后每项 200px,三栏等宽——因为我们主动丢掉了各自的内容宽度。 - 都写
flex: auto(即1 1 auto):基准是 100、200、50,共 350px,富余 250px 平分,每项分到 83.3px,结果是 183.3px、283.3px、133.3px,仍然不等宽。
所以“给每项 flex: 1 就等宽”成立的前提是 basis 归零,而不是 grow 等于 1 本身。
只写 flex-grow: 1 和写 flex: 1 也不一样。 flex-grow: 1 只动了 grow,flex-basis 还是初始值 auto,等于 1 1 auto——也就是 flex: auto 的效果,起步宽度还带着内容。很多人写下 flex-grow: 1 期待等宽三栏却没得到,原因就在这里。
这也是 MDN 建议优先用简写关键字(auto、initial、none)或单位数字的原因:单独改一个长写属性,很容易留下一个自己没想清楚的组合 35。
怎么算出一项最终多宽
按顺序问四个问题就够了:
- 基准多大? 看
flex-basis;是auto就退回width,再退回内容宽度。 - 空间够不够? 基准尺寸之和与容器主轴尺寸相减,得正走 grow,得负走 shrink。
- 分多少? grow:富余空间按 grow 因子之比切分,加回基准;shrink:缺额按“shrink 因子 × 基准尺寸”之比扣除。
- 有没有到底? 缩不下去时检查
min-width: auto这道下限,必要时改成0。
把这三条长写属性记成一句话:flex-basis 决定从哪儿出发,flex-grow 决定多出来的怎么分,flex-shrink 决定不够的怎么扣;而 flex 简写最大的坑,是省略的 basis 会变成 0% 而不是 auto。