5

什么是 BFC

W3C 为浏览器规定了三种定位模型:Normal Flow, 浮动, 和绝对定位。本文所介绍的 BFC (Block Formatting Model) 是属于 Normal Flow 中的一小节内容,并且这部分内容只有三段话。第一段:

Floats, absolutely positioned elements, block containers (such as inline-blocks, table-cells, and table-captions) that are not block boxes, and block boxes with 'overflow' other than 'visible' (except when that value has been propagated to the viewport) establish new block formatting contexts for their contents.

简单来说,BFC 仅仅是一种定位的情况,即当元素满足 BFC 的条件时,浏览器怎么去显示它。上面这短话讲的是如何建立 BFC,只要满足其中任何一个条件就可以了。可能需要解释一句话,就是:block containers that are not block boxes。

block box 是指块级元素,就是会强制换行的元素,比如 div。block container 是指可以包含块级元素的元素,但它不一定要换行,比如 div, td。可以看到,后者的范围更大一点,两者异或的结果就是这句话的意思了。除了规范上列举的 inline-block、table-cell 等,主流浏览器还支持 flex 。

建立 BFC

默认的 div 是块级元素,并不是 BFC,所以需要一些额外的样式来生成。按照规范,我们大概有如下(不止)的手段,以及相应的代价:

  • disblay: table; 无法解决响应式的问题。

  • display: inline-block; 会使后续内联元素显示在同一行。

  • position: absolute; 将元素从 normal flow 中剥离出来。

  • float: left; 将元素推向左侧。

  • overflow: scroll; 会出现滚动条。

  • overflow: hidden; 会截断溢出的内容。

请根据不同的实际情况,选择最合适的方式。

仔细想一下,在 normal flow 的章节中写道,用 float: left; 或者 position: absolute; 去创建 BFC,是不是有点矛盾?不然。BFC 指的是为子元素创建一个定位环境,浮动和绝对定位是针对该元素本身,而不是其子元素。

外边距合并

在 normal flow 中,我们知道,垂直方向上相邻元素的外边距取两者中较大的值,例子:

<div class="container">
  <div class="c1">第一块</div>
  <div class="c1">第二块</div>
  <div class="c1">第三块</div>
</div>

相应的 CSS:

.container {margin:20px;border:1px solid #212121;width:300px;background-color:#009688;}
.c1 {margin:10px 0;height:40px;background-color:#FFC107;}

结果示意图:

边距合并

可以看到,每个黄色块之间的距离是 10px,而不是相加的 20px 。扩展一下,如果用 .c1 {margin:10px 0 15px;} 那么黄色块之间的距离就是 15px,取较大值。如果是 .c1 {margin:10px 0 -15px;} 呢?是 10px,还是 0px,还是 -5px?

关于边距合并,规范中是这么描述的:

Vertical margins between adjacent block-level boxes in a block formatting context collapse.

也就是说,边距合并的条件是,子元素处于相同的 BFC 中,如果为其中一个子元素创建新的 BFC,就不会发生合并的情况了。

<div class="extra">
    <div class="c1">第三块</div>
</div>

现在为第三块添加一个额外的父元素,并且为该元素创建 BFC:

.extra {overflow:hidden;}

图不单独给了,这里给个问题,就是,如果不添加父元素,直接把 .extra 类加给第三个块,如下:

<div class="c1">第一块</div>
<div class="c1">第二块</div>
<div class="c1 extra">第三块</div>

可以防止边距合并吗?不行。再次重申一下,BFC 是为元素创建定位环境。

BFC 的应用

清除浮动

如果一个块级元素的所有子元素都浮动了,那么它的高度就为 0 。

.c1 {float:left;margin:10px;width:100px;height:50px;background-color:#FFC107;}

对于同样的 html,修改 .c1的样式,添加浮动的效果,可以看到

浮动

父元素只剩下了边框,并没有 content height 。如果要清除浮动,可以为容器添加伪类:

.container:after{display:block;content:'';clear:both;}

这样,父元素的高度就又回来了。

通过 clear 清除浮动

当然,我们也可以为父元素添加 overflow:hidden; 来清除浮动。

.container {overflow:hidden;}

这两个方法都可行,前者很直白,clear:both; 本来就是用于清除浮动的,为什么用 overflow:hidden 也可以?因为这样就创建了一个 BFC,对于子元素的排列,规范是这么写的:

In a block formatting context, each box's left outer edge touches the left edge of the containing block. This is true even in the presence of floats, unless the box establishes a new block formatting context (in which case the box itself may become narrower due to the floats).

子元素的外边缘会触碰到父元素的左侧(严格来说是,子元素外边距和父元素的内边距)。如果父元素的高度为 0,那么就违背规范了。

所以为父元素添加浮动、绝对定位等任何用于创建 BFC 的方式,都可以清除浮动。

防止文字环绕

常见的一种布局就是,特别是评论回复中,会看到在头像右侧有一段文字,就算文字很长,也不会出现在头像下方,会一直保持在右侧。

非文字环绕

这样的 HTML 格式很简单:

<div class="container">
  <div class="avatar"></div>
  <div class="comment">...</div>
</div>

.comment 中的内容省略不写了。如果仅仅对 .avatar 左浮,就会出现文字环绕现象:

文字环绕

解决的办法可以是为 .comment 添加一个适当的左外边距,比如 90px 。

另一个办法是利用 BFC 。

根据规范,子元素的外边距必须向左靠着父元素的内边距,也就是说,尽管 .avatar 浮动了,但是 .comment 还是会靠着父元素的左侧内边距,它们两者是上下重叠的!

unless the box establishes a new block formatting context,这句话表明,只要为 .comment 创建 BFC,那么这个约束就可以被打破,即它们不会重叠。

in which case the box itself may become narrower due to the floats,由于浮动元素占据了一定的宽度,新创建的 BFC 会因此而变窄。

.avatar {width:70px;height:70px;float:left;margin:10px;background-color:#ffc107;}
.comment {overflow:hidden;}

多列布局

一般来说,如果要做宽度自适应的多列布局,就会用百分比做宽度单位,并且子元素浮动。那么问题就是,如果百分比换算到像素时,一旦子元素的宽度之和大于父元素,最后一列就会换行。

<div class="container">
  <div class="column"></div>
  <div class="column"></div>
  <div class="column"></div>
</div>

CSS3 本身可以实现多列布局,用 columns 属性或者 flex 布局。这里只是为了更好地去理解 BFC 而做一个例子。

.column {float:left;margin:0 1%;width:31.33%;height:100px;background-color:#ffc107;}

先来算一下,左右边距各 1%,宽度 31.33%,三列布局,总共 99.99%,应该不会超。但对于部分浏览器,宽度换算时是四舍五入的,比如,父元素的宽度为 500px,那么四舍五入,每列宽度为 157px,左右边距 5px,总和 (157 + 5 2) 3 = 501,正好超 1px,最后一列换行。

最后一列要换行是因为浮动元素的特性,如果它不是浮动元素,那么就可以按照 normal flow 的规则来,即,向左靠至父元素左边距。

然后,根据上个小节的知识,如果为元素创建新的 BFC,那么“向左靠”的规则可以打破。两个条件结合,得到:

.column:last-child {float:none;overflow:hidden;}

多列布局

虽然可以防止换行,但是最后一列的间距有点不一致。

本文主要是讲了 BFC 的两个关键点,第一:如何创建,以及每种方式的代价;第二:特性,防止默认的边距合并和向左靠至父元素内侧。很多衍生用法都是基于这两种特性来的。

不过,往往我们是直接去记用 overflow:hidden; 可以清除浮动,并没有想为什么,毕竟内容不多。有时候想想原理,也挺有意思的。


名一
2.9k 声望522 粉丝