CSS 排版与正常流 —— 重学 CSS

同学们好,我是来自 《技术银河》的 💎 三钻 。
这一周我们重新回到《重学 CSS》系列,之前的文章中我们重新学习了《 CSS 选择器》和《 CSS 语法与规则》。接下来我们就一起来讲讲 CSS 里面的排版与正常流
在讲解 CSS 当中的排版和正常流的时候,我们会按照属性的一些逻辑关系来分成几个部分来讲解与学习。

盒 ( Box )
讲到排版,我们需要引入的第一个概念就是 "盒
"。之前我们在《模拟浏览器》和之前的一些 CSS 的文章中都讲到了排版相关的概念。
而我们真正去讲到排版的时候,我们需要用到的单位一定就是 "盒
"。
在真正进入详细了解 "盒" 的概念之前,我们先来做认识一下 3 个比较容易混交的概念。
标签 ( Tag ) —— 源代码
元素 (Element) —— 语义
盒 ( Box ) —— 表现
HTML 代码中可以书写开始
标签
,结束标签
,和自封闭标签
。
标签是一个源代码的概念,所以方式我们提到在 HTML 代码中写的肯定都是标签。
一对起止
标签
,表示一个元素
。
元素是存在我们脑子里的一个概念,它是语义领域的一个概念,所以一对起止标签它一定是表示一个我们脑子里面的概念。
DOM 树中存储的是
元素
和其他类型的节点 ( Node )。
DOM 树中存储的不全是元素,因为 DOM 树中存储的东西叫节点 Node
,所以元素只是是节点的一种。
比如说我们的文本节点也是节点,但他并不是元素。再比如我们的注释节点,它也是节点但是它也不是元素。当然还有 CDATA 节点,还有 processing-instruction,DTD 等这些都是会存入 DOM 树的,当时它们都并不是元素。
很多同学的理解,DOM 树中存储的都是元素,不过这样也没有错。因为其他的节点相对来说都没有那么重要。
CSS 选择器中的是
元素
。
其实这里还可以加一个 "或",在《CSS 选择器》中讲到的,CSS 选择器选中的是元素或者是伪元素
。
CSS 选择器中的
元素
,在排版时可能产生多个盒
。
这个地方是大家需要注意到的一个概念,CSS 选择器选中的元素,它不一定和盒是一一对应的关系。它有可能是一对多的关系的。但是有盒一般来说必定是有对应的元素的。我们不可能无中生有产生一个元素,即使是号称是无中生有的伪元素也是依附于一个选中的元素产生的。
排版和渲染的基本单位是
盒
在我们的《模拟浏览器》的实现过程中,我们的排版盒渲染都是直接拿元素当盒去用了。但是这是一个很粗糙的做法,在实际上我们很多元素都会产生多个盒。
比如说 inline
元素就会因为分行而产生多个盒。又比如说带有伪元素,被伪元素选择器选中的元素也会生成多个盒。所以我们排版盒渲染的基本单位都是盒。

盒模型
既然我们讲到盒,我们都会讲到大名鼎鼎的 "盒模型
"。我相信很多同学都知道盒模型,并且也学习过盒模型。但是也有很多同学可能没有理清楚摸透这个概念,所以就会导致不知道什么是盒模型,更不知道"盒
"这个概念是从何而来。
上面我们已经讲清楚了盒是从,标签到元素,到 CSS 选择器到如何产生了盒。所以对盒的来龙去脉我们都很清楚了,所以这里我们就可以开始详细的去了解盒模型的概念。
盒模型是我们排版的时候所用的一种基本单位
盒模型中的"盒",不光是有一个宽和一个高。不像在《模拟浏览器》部分里面那样非常的好算,其实它还是挺复杂的。
盒模型是一个多层的结构,从里面到外面分为:
最里面就是
content
,也就是我们的内容
content 到 border 之间有一个圈空白,这个圈叫做
padding
,也就是内边距
Border 的外面又有一个圈空白叫
margin
,也就是外边距

padding 主要影响的是盒内的空间 —— 主要决定盒内的空间排布,也就是 content 区域的大小
margin 主要影响的是盒外的空间 —— 决定了盒周围空白区域的大小
盒模型里面的 宽 (width) 是有讲究的,盒子的宽度是有可能被 box-sizing
属性所影响的。最常见的两个值就是:
content-box
设置的 width
属性只包含 content
的内容的空间。也就是说:
盒子占用的空间
= content 的大小
+ padding 的大小
+ border 的大小
+ margin 的大小

怎么听起来就是一脸懵的感觉。😂
其实更接近人类的理解就是,我们在 CSS 中设置的
width
属性只对最里面的content
的空间有效。其余的padding
,border
和margin
都会叠加到盒子的占用空间。
那这个为什么程序员都说这种盒子是 "反人类" 的呢?如果很早起就接触到 HTML 和 CSS 的同学应该都知道这么一个让人痛不欲生的场景:
在排版时候我们明明设置好这个盒子的宽度,但是最后加了 border 和 padding 就让盒子 "变大" 了。所以最后我们要反过来重新计算 width 属性来保证这个盒子是我们想要的宽度。
对!可能很多用习惯 box-sizing: border-box
的同学,早就忘记了这些痛苦的日子了。但是事实上这个设计理念就是有点反人类的。所以后面就打了一个补丁来拯救我们程序员,加入了 border-box
。从此之后程序员就又可以开心的敲代码了。

border-box
使用border-box
,我们的 width 就包含了 padding 和 border 的尺寸了。回去看看我们盒模型的图,我们可以看到border-box
的黄色线括着的区域,它所占据的空间和范围。
这样当我们给一个盒,padding 和 border 的时候,就不会影响我们给予盒子的 width。这样我们就可以保证我们盒子在没有 margin 的时候它所占据的空间就是与我们 width 一致的。
呈现出来的效果就是 padding 和 border 都会往内挤压空间,而不会影响盒子的宽度。
这里我们就讲完盒模型了,我们就发现它所影响的属性就是
margin
,padding
,border
,box-sizing
这几个属性。这些都是影响我们盒模型的总体尺寸,在排版中会影响着这个盒模型所占据的范围。

正常流
CSS 的排版其实是有三代的排版技术的:
第一代就是
正常流
第二代就是基于
Flex
的排版第三代就是基于
Grid
的排版结合最近推出的
CSS Houdini
,可能更接近的是 3.5 代,它是一种完全自由的,允许使用 JavaScript 干预的排版
目前主流都是在使用 flex
布局。相比 flex
,其实正常流并没有变得更简单,反而是更复杂了。
不过挺有意思的是,flex 它比前面的第一代的排版技术要简单,比他后面一代的 grid 也简单。个人认为 flex 是最简单并且最容易理解的一代排版技术。
正常流呢,其实它能力最差,但是反而他的机制很复杂。
排版

如果我们看到上面这个图,可能有一些同学知道是什么,有一些同学完全不知道他在干什么。其实这个就是 80 年代印刷厂工人在进行排版工作。
这个传统的排版技术,其实与我们现在网页的layout
是息息相关的。在很多文章中,我们会把layout
翻译成排版
,有时候也会翻译成布局
。但是我个人也觉得翻译成排版
是最贴切的。因为 CSS 当中的layout
是源自于传统的排版技术。
传统的排版方式,我们需要先把字版放入一个一个字框里面,按照文字的顺序排列号,然后再把这些字框一个一个的排列进我们的排版框里面。
所以所谓排版就给我们所有可见的东西放到正确的位置上去。而在 HTML 里面,我们是有 "盒" 这样一个东西,在 CSS 的排版里我们只排两样东西
。
盒
文字
一切 CSS 的排版,都不会逃出这
盒
与文字
这样两东西。所以我们的排版就是给每一个文字安排到正确的位置上,然后给每一个盒安排到正确的位置上。
在不考虑盒模型的情况下,我们需要关注的就是位置和尺寸,所以排版中并没有什么特殊的内容。
字排版

我们在进入正常流之前,我们一起来先思考一下我们写字的时候是怎么写的。
其实我们写字的时候,某种意义上讲也是一个排版的过程。很多同学们小时候写作文的时候都会用到上面这种有格子的稿纸。我们都记得这个作文稿纸上都是有一个一个的格子的,其实这相当于一个已经有了排版。
如果在我们稿纸上没有格子呢,我们就要自己来决定每一行要写多少个字,每一个字有多高,然后怎么分段呢?等等这些问题,都是在我们小时候写字时需要关注的。
那么我们来总结一下写字有哪些规则呢?
从左到右书写 —— 我们都是依次从左向右书写作文的
同一行写的文字都是对齐的 —— 写中文的时候我们是对齐到格子的顶和底,写英文的时候,像 e,a 这些字母它就有一条基线要去对齐。所以小时候英文的书写本都是四线本。
一行写满了,就换到下一行
其实以上的这些规则也就是我们说的 "正常流 ( Normal Flow )
",所以正常流为什么正常呢?因为正常流就是与我们平时书写文字的习惯一致。无论是中文也好,英文也好,它们都是遵循这种自然的排版方式的。
有一些我们早期进入前端的同学,就会发现其实正常流里面,有很多特别不正常的东西,特别的反直觉,反人类的东西。为什么这么奇怪的东西要叫正常流呢?

很负责任的告诉大家,其实一点都不奇怪。如果我们知道前端的 HTML 和 CSS 的排版概念都是源自于专业排版知识,而这些排版使用方式如果出现在我们的书籍里面,我们都会觉得挺自然的。但是真正让我们去理解这些知识,我们都会觉得很困难。
这个就可以追溯到 HTML 最早期整个的排版设计,都是从文字出版行业过来的专家所做的。所以它使用的思路都是那个时代的一个专业的思路。跟我们自然人脑子里面的理解,可能就会有一些差异了。
正常流排版
接下来我们就正式进入正常流的排版讲解。
正常流排版的整个过程,与实现 flex 的过程比较类似,有这几个步骤:
收集盒与文字进行
计算盒与文字在行中的排布
计算行与行之间的排布
我们发现其实这个与我们 flex 的排版非常的像。其实我们会发现所有的排版算法,基本上都是差不多的。不论是哪个软件,哪个规则,它们都是这么几个步骤。
接下来我们来看看具体的一个排布规则:

当文字和盒在一行里面的时候,它们是会从左向右排布的。假设我们先不考虑 writing-mode 的事情,这里所说的从左向右排就意味着文字和盒有一个对齐的规则。这个就比我们小时候写作文的那个格子要复杂了。
在写作文的格子稿纸上,我们是不需要考虑图片和其他元素与我们的文字混排的情况的。我们在写作文的时候不会写着写着在旁边话一个插图什么的。但是我们发现书里面是有的,特别是专业的书籍基本上都是图文内容。
这些书里面都是写着写着就会插入一个图表,这些图表有的是在行内放一个小的图片,有的是里面加一个小图标。这些都是行内盒,更准确的说这叫 inline-level-box
,就是行内级别的盒。
还有一些我们需要插入一个大图,比如说一个统计的数据,因为这种一般来说高度都比较高,放在一行里面会很奇怪。所以这个时候我们会让它单独占一行。这种类型的盒,我们就把它称为 block-level-box
,也叫块级盒。
所以文字和 inline level 行内的盒排出来的行,我们就叫做 "行盒 ( line-box )
",然后一个自然的正常流,整体来看就是一个个的 line-box
和 block-level-box
的从上到下的摆布。如果没有 block-level-box
,那么都是行盒从上到下排布。而每个行盒的内部都是从左到右的排布。
其实上面讲到的两种情况都是有一个名字的。

块级排布的我们就叫
BFC
—— Block level formatting context (块级格式化上下文)行内排布的我们就叫
IFC
—— Inline level formatting context (行内级格式化上下文)
很多在比较早期学习前端,甚至在一些 "0 基础" 的班里面学前端的同学,都会听说过 "块级元素" 和 "行内元素" 两种说法。其实与我们讲到的 BFC 和 IFC 就是这个概念最原本的意思。
行级排布

小时候我们在学习英文的时候,就会使用过这种 4 线本的格式来书写英文。
我们可以注意到在书写 example
这个英文的时候,我们可以看到 e
,x
,a
,m
都是在一行之内的。但是 p
和 l
就不一样了。p
的尾巴是稍微往下突出的,而 l
它的头部是稍微往上突出的。
所以英文的字母呢,总是会有一些字母会往上或者往下伸出一块的。这个也是英文书写的一些规范,因为遵循这些规范才能让我们的字母在书写的时候好看。基本上所有字母的下缘都是需要对准一条线的,那就是倒数第二条钱(在图片中的黄色的线)。
这条线也叫 "
基线
",而这个规则就叫做 "基线对齐
"。
其实各国的文字都是有一条基线对齐的规则,但是不同的国家的文字依赖的基线的位置不一样。比如说中文需要和英文混排的话,就会出现一个基线的偏移。
像中文这种类型的字,都会以上文下文这种边缘作为基础线去对齐的。但是我们也是可以认为这些类型的文字也是基于 基线 (baseline)
对齐的。我们可以理解为它们只是带了一定的偏移。
字形定义
接下来我们一起来了解一下,一个字形它是怎么样去定义的。在之前的文章中我们有讲到一些字符集这方面的基础知识。其实一个字符就是一个码点,以为着它的形状是由我们的字体来决定的。
下面的两张图是来自于一个著名的 C++ 的底层库,叫做 freeType
。这个库就是处理各种字体文件成为抽象,并且它是一个开源界大部分的软件都会去使用的一个字体库。
这个库中,它从字体中抽象出这么一个定义:
任何一个文字,都是有一个宽和高,除此之外还有一条基线的定义。(如何没有这个基线的定义,我们的字体是不成立的,也没有办法用它来进行排版)。这个定义就叫做
Glyph Metrics
。
我们先来看看第一张图:

首先我们看到 origin
,也就是一个字体的原点。而原点标识的位置就是我们文字的基线的位置,而这个文字都是以基线作为原点的坐标,然后再用这个坐标来定义这个文字的位置。
所以说基线就是在 x = 0
这个横向的坐标,然后这个文字就会有一个 xMax
和一个 yMax
,xMax
就是文字最尾端的坐标位置,而 yMax
就是文字最顶端的位置。
然后文字也有相反的两个值 xMin
和 yMin
,而这两个值就是刚好相反,是文字开始的位置和底部的位置。
也可以理解为从底缘的基线到文字的最顶端的距离,这就是
yMax
。而底缘的基线到文字的最底端的距离就是yMin
。而xMin
到xMax
,从原点到文字开始和文字结束的距离。
最后这里面的 advance
就是整个字占用的空间。
以上讲的就是横向排版的时候,而纵排又有另外一套的逻辑:

我们需要理解一些文本这样的基础概念,然后我们才能更好的理解文本和盒的混排。
行模型

我们大致了解了这个文字在字体里面是如何定义之后,我们就可以来讲讲 CSS 里面的行模型了。这里我们重点讲 5 条线,其他还有一些线和位置例如 sub
和 sup
这种我们就不去讲了。
这个 base-line 和文字的顶原和底缘,分别叫做 base-line
、text-top
和 text-bottom
。就是图中的一条黄线和两条绿线。
结合刚刚讲到的,base-line
就是用来与英文字母对齐的。然后这里面有一个 text-top
和一个 text-bottom
,如果我们的字体大小不变,这两个线是不会变得。
如果我们使用多个文字的字体混排的话,这个 text-top
和 text-bottom
是由 fontSize
最大的一个字体决定的。然后这个文字的上缘和下缘可以理解为两条固定的线。
如果行高是大于文字的高度的时候,我们就会有 line-top
和 line-bottom
,就是在图中的两条白色的线。
好!如果我们只有文字的话,这个行模型就这样子排布的。但是我们一旦涉及到跟盒的混排,就会涉及到一个 line-top
和 line-bottom
的偏移问题。

当我们的盒足够大的时候,我们的盒子是从 text-bottom
去对齐的,所以它就有可能把这个高度撑开。这个时候 line-top
就会从虚线的位置,移动到了白色的实线的位置。(看上面的图)
这个现象也是在正常流当中,处理行模型中非常麻烦的一个现象。因为盒的先后顺序和尺寸都是影响
line-top
和line-bottom
的位置。但是盒是不会影响text-top
和text-bottom
的。
代码演示
接下来我们来一起看看在浏览器中的实际效果是怎么样,需要在浏览器上看效果,我们就需要些一段代码了。
我们先建立一个
wrapper
把我们的内容包裹起来,然后给一个背景颜色用
span
标签加入文字在用
div
标签建立一个元素,然后给予这个元素display: inline-block
,把元素设置成行内盒
最后我们加入
基线
的元素base-line
,用这个来展示我们的基线是在哪个位置
最后呈现出来的效果如下:

这里我们发现,我们的
行内盒
是默认与基线对齐的规则。也就是说盒的下边缘会和文字的基线去做对齐。
好,如果我们在盒子里面加文字又会怎么样呢?我们来试试看。
这里我们在盒里面加入一个 b
的文字。
最后呈现出来的效果就是这样的:

这里我们会发现盒子的对齐的位置发生了变化。盒的基线变成了它里面文字的最后一行的基线。也就是说,当一个盒子里面有文字的时候,这个盒子的对齐就会基于里面文字的基线做对齐。
这里如果我们在 b
的下一行加一个文字 c
,我们又会发现另一种现象。

这个是特别需要注意的问题,行内盒
inline-block
的基线是随着自己里面的文字去变化的。所以说大部分情况下是不建议大家给行内盒使用基线对齐的。
所以我们在使用行内盒
的时候就需要给一个 vertical-align
,属性值我们可以给 top
,bottom
或者是middle
都是可以的。
我们先看看 vertical-align: top
—— 顶缘对齐

给
top
就是跟行的顶缘对齐,因为我们的外包框给的是 100px,所以它会把这一行撑开。
然后我们来看看 vertical-align: bottom
—— 底缘对齐

与顶缘对齐不一样就是这个时候,盒就会从下面往上撑开。
最后我们看看 vertical-align: middle
—— 中线对齐

顾名思义,这个时候盒就会与文字的中线对齐。
其实这里我们还可以让盒与文字的顶缘和底缘对齐,也就是 text-top
和 text-bottom
。
我们来看看 text-bottom
是怎么样的:

盒一样会把上边缘和下边缘撑开。
如果我们看到上面的如,紫色的线就是我们的基线,但是我们也可看看文字的中心线
、顶缘线
和底缘线
的位置。我们只需要改动 base-line
这个 div
元素的 vertical-align
属性即可:
我们先看看中线:

顶缘线:

底缘线:

这里我们可以观察到,我们的盒是把我们的
顶缘线 (top-line)
往上撑开了的,所以也证明我们行内盒是会把顶缘或者下缘撑开的。
为了让大家可以更加直观的看到顶缘和底缘被撑开的现象,我们可以加多一个行内盒,同时把顶缘和底缘同时撑开。

这里我们可以明显发现,我们外包框的高度已经被撑开了,所以外包框的高度已经不是等于行内盒的高度了,应为它的顶缘和底缘线都被撑开了。这个显现就是行内盒不同的
align
对齐所导致的,所以不同的vertical-align
对整个行的高度是有比较多的影响的。所以相对于flex
布局,flex
就只需要去考虑最高的一个元素,但是在正常流里面的行模型还是比较复杂的。
最后我给大家附上这段代码的演示:
块级排布
接下来我们来一起了解一下,正常流的块级排布。我们在上一部分已经了解了块级排布了。接下来我们一起来了解行级与块级的盒之间是如何排布的。
之前讲到的 BFC 和 IFC 的基本定义的时候,我们对块级排布已经有一个基础的认识了。但是呢正常流当中还有两个非常复杂的机制,我们需要先了解一下。
Float 和 Clear
Float 和 Clear 也称为 浮动
与 清除浮动
。首先浮动元素严格来说已经脱离了正常流,当时他又依附于正常流去定义的一类排布方式。
首先我们先来讲一下 float
的基本规则。根据 W3C 的标准,float 可对它有一下定义:
float 元素可以先排到页面的某一个特定的位置,同时可以当它是正常流里的元素。然后如果它的属性中有
float
的时候,这个元素就会朝着float
属性定义的方向去挤。
假设元素加入一个 float: left
属性,这个时候,这个元素就会往左边去挤,如下图:

通过上面这个动画,我们会发现,原来已经存在的文字位置就被浮动了的元素所 "盖住" 了。所以呢这个时候就会根据 float 所占据的区域去调整行盒的位置。因为计算位置的时候我们还没有去计算每一个文字具体的位置,所以说理论上来讲这个地方的文字是没有重排的。
所以当一个元素变成了浮动时,它所占据的位置的原本内容,就会根据 float 之后占据的宽度来进行调整。而 float 显著的特征就是,它会影响我们生成这些行盒的尺寸。
所以在文字调整了之后我们最终看到浏览器呈现的效果是这样的:

float 不止会影响自己所在的行,凡是它的高度所占据的范围内的所有行盒都会根据 float 元素的尺寸调整自己的大小。如果超出了这个 float 范围的就不考虑了。

如果我们有两个 float 元素的时候,还会出现一种情况。假设我们现在有两个 float 元素,一个是 float: right 在右边,然后再后面又加入了一个 float 元素,而这个新的 float 元素一样也是向右浮动的。
这个时候我们会发现这个新的 float 元素也会受到上一个 float 元素影响,新的 float 元素是无法占据上一个 float 元素的位置。这时候的这个现象就是 float 元素相堆叠的效果。

当让如果我们继续添加
float
元素,并且也是浮动一样的方向的时候,都会受到上面多个float
元素的影响并且继续堆叠。
接下来我们来看看一段实例代码:

这里面 「float-放置的位置」
就是原本这个 float 元素的代码所放置的位置。但是经过了 float 之后,所有的 float 元素都会被挤到最右边的位置。最后所有行盒的生成都会围绕着所有的 float 元素,同时它们都不会占据 float 元素的空间。
这个时候当我们把外框的宽度增加的时候,就会发现这些 float 元素就会出现堆叠的现象。

看到这里肯定很多同学说觉得 "这样排版不行呀,老板肯定不收货呀","UI 设计师肯定要吊打我们啦"。是的这样的排版确实是不正常的,而且一般来说也没有这么排版的需求呢。
所以这里我们可以给 float 加一个 clear 属性,这样我们就可以让它们强制换行了。

所谓的 clear
有的翻译成 "清除浮动",但是我觉得它不是清除浮动的意思。理论上来说它是 "找一个干净的空间来执行浮动" 的意思。
比如说 clear: right
,就是要在右边找到一个干净的空间来执行这个浮动的操作。
所以看到上图的效果,当我们加入了 clear: right
之后,第一个后面的元素都不受第一个 float 元素的影响,各自找到一个干净的空间进行浮动。所以 clear
是有分 left
和 right
,指的是向左或者右找干净的空间进行浮动。
虽然说 float
这种浮动布局给我们带来了不少的麻烦,但是 float 的堆叠、自动换行(或者使用 clear 去换行)的行为是可以帮助我们去进行一些有用的布局的。因为我们的正常流的布局在早年没有 flex
的情况下,正常流的布局下完成一些著名的 CSS 布局需求的时候是非常的困难的。
所以大家都产生这种使用 float 来代替正常流的 inline-block 来进行布局的技术。
接下来我们看看下一段代码实例:
这里我们给每一个元素都加上一个 float
,这时候元素跟元素之间的表现就很像一个正常流了。因为 float 元素它占满了之后还是会换行的。那么我们来看一下代码:

这里我们可以看到,float 是完全可以变成和正常流一样排成一行的,当我们缩小宽度的时候,不够位置的元素就会自动换到下一行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UsH8GwbC-1602443824687)(/Users/tridiamond/Library/Application Support/typora-user-images/image-20201006194359630.png)]
这个时候如果我们想执行一个强制换行怎么办呢?这个时候我们就可以拿出我们的 clear
。比如说我们想在 3
后面开始强制换行,我们就可以在 4
的浮动元素上加入 clear: left
即可。
这里要注意的是,float 它是不认
<br/>
的,如果我们在一个 float 元素的后面加入<br>
是无法让他强制换行的。因为br
是正常流的换行,对 float 是没有效的。

我们平时做布局的时候,基本上我们都会使用盒而不是文字。我们很少有真正意义上需要像原始的 float 设计一样用图文混排的设计(就是向我们看的图书一样的布局方式)。在网页上布局,一般来说我们都是针对同一个级别,我们都是几个盒子排成正确的形状来完成页面上的布局。
所以说 float 这种布局的技术,在比较早期的时代是非常流行的。不过到了现在的前端技术时代,float 的布局方式已经完全被 Flexbox
技术所替代了。这里只是让大家知道一下,布局的历史里面有这么一个用法。
如果有同学遇到了一些需要维护非常古老的代码,就有可能看到这个技术。但是我不推荐大家当今还去使用这项技术。
Margin 折叠
接下来我们来讲讲正常流里面的一种现象,叫做 margin 折叠
。在 BFC 里我们的元素是顺次从上往下排的,但是顺次从上往下排的时候还是会受它的盒模型影响的。
就是有这么一个现象,在一个从上往下排布的 BFC 里面,有一个元素它有 margin,接着还有一个元素,它也有 margin。那么这个时候第二个元素它应该怎么排呢?
直接告诉我们应该是两个元素的 margin
是应该会叠加,如果第一个的 margin 是 10,第二个是 15,那么两个元素中间就是 10 + 15 = 25。对吧?不是的。

它们并不是把两个 margin 的空白都留出来。而是会让他们两个发生一个堆叠的这样的现象。最后叠出来的高度是跟最大的 margin 的高度相等的。如果一个是 10px,一个是 15px 的 margin,最后两个元素之间的空间就是 15px(使用了两个 margin 的最大值)。这个现象就是
Margin Collapse (留白折叠/边距折叠)
。

有些同学觉得真的是匪夷所思,根本不符合常理。但是其实这个是一个排版里面的要求,因为在我们的排版当中,任何一个元素,它的盒模型里面所谓的 margin "只是要求周围有这么多的空间是留白的,而不会说要求元素与元素之间的边距格子都有相对应的空白"。所以只要元素的周围的留白的空间够了,自然就是一个合理的排版方式。
这个可以说也是继承了印刷行业,古老的印刷行业的排版体系中,这种 Margin Collapse
是一个非常自然的现象和思路。
我们要注意,这种
Margin Collapse
只会发生在 BFC 里面。它不会发生在 IFC 或者其他的排版方式里面,比如说 flex、grid 等都不会有Margin Collapse
的。所以只有正常流中的 BFC 会发生边距折叠!
其实我们单看 float 和单看边距折叠,它们都不太难,我们但看 BFC 也不太难。但是如果我们三个现象叠加在一起,基本上就是我们古代前端最大的难题了。也就是说大家平时会看到一些资料里面讲,面试的时候遇到的 BFC 问题就都在这个三个现象相叠加的。
BFC 合并
接下来我们来认识一下正常流最困难的一部分,"BFC 合并
"。在讲到具体的知识之前我们先来了解一下几个概念。
BFC 代表的是
Block Formatting Context (块级格式化上下文)
。
Block (块)
我们先大致了解一下 Block
里面有哪些:
Block Container —— 是在 CSS2.1 标准里面定义的
+ 里面能装 BFC 的盒
+ 能容纳正常流的盒,里面就有 BFC
Block-level Box
+ 外面有 BFC 的盒
+ 也就是说它能够被放入 BFC 的这种盒子里
Block Box = Block Container + Block-level Box
+ 就是上面两个之和
+ 里外都有 BFC 的盒
这里我们就逐个拆开来讲一下:
Block Container
Block Containter 基本上是一些 display 的效果:
block
inline-block
table-cell —— 里面都是正常流,但是
table-row
就不是block cotainer
了,因为它里面是 table-cell,所以不可能是正常流flex item ——
display: flex
的元素不是block container
,但是 flex 的子元素flex item
如果它们没有特殊的 display 属性的话它们都是 block container。grid cell —— grid 也是有 cell 的,所有 grid 的 cell 默认也都是 block container
table-caption —— table 中有 table-caption (表格标题),它里面也是正常流
任何一个元素里面只要不是特殊 display 模式的,它里面默认就是正常流。
Block-level Box
我们大多数的元素的 display 的值都是有一对的。一个是 block-level
的,一个是 inline-level
的。

在 display 的最新标准里面,已经把
block-level
和inline-level
拆成了单独的属性。Display 会出现这个inner
和outer
,这里就是给 display 做了系统性的分解。
这里还有一种非常特殊的 display 属性叫
run-in
。它跟着自己上一个元素来,有时候是 inline-level,有的时候是 block-level。这个属性我们知道就可以了,在工作中基本上没有见过有例子有运用到run-in
这个属性。
Block Box
在标准里面有这么一个描述:"什么时候什么样的盒,会创建 BFC 呢?"
设立 BFC (Establish BFC)
以下情况下会 设立 BFC
- floats —— 浮动的元素里面就是一个正常流,所以会创建 BFC
- Abusolutely positioned elements —— 绝对定位的元素里面也会创建 BFC
- block containers (such as inline-blocks, table-cells, and table-captions) that are not block boxes —— 就是这个
block container
但是不是block box
(也就是不是 block-level ) 会创建 BFC,包括以下:- inline-blocks
- table-cells
- table-captions
- Flex items
- grid cell
- 等等...
- _and block boxes with
overflow
other thanvisible
_ —— 就是拥有overflow
属性,但是不是visible
的block box
也会创建 BFC
所以设立 BFC 一共有 4 大类,但是要记住这些确实不太好记。不过换一种方式来理解就好记一点了。
默认这些能容纳正常流的盒,我们都认为它们会创建 BFC,但是只有一种情况例外:就是 block box 里外都是 BFC 并且 overflow 是 visible。用公式来记就是这个:"block box && overflow:visible"
这个其实是非常合理的,它里外都是 BFC 而且它的 overflow
是 visible
,那不就相当于没有 BFC 了吗?所以这个时候会发生 BFC 合并。
BFC 合并
那 BFC 合并之后会发生什么呢?这里我们就需要了解两个最重要的 BFC 合并之后的影响:
BFC 合并与 float
因为 BFC 发生了合并,所以里面的行盒跟这个 float 就有了一定的影响。
正常的来讲我们放一个 block box 它的 overflow 不是 visible,这个时候它会创建 BFC 并且整个 block box 放进 BFC 里面。那么整个就会受 BFC 影响,如果不创建 BFC 它里面的行盒就会受 float 的影响。
下来我们用一段代码来看看其中的现象:
这里因为我们的
text
盒子给予了overflow: visible
,所以这个overflow
属性值是不满足创建 BFC 的条件的。所以我们的文字的盒子就会像不存在一样,文字就会环绕着外面的float-box
元素来进行排布。

如果我们把 overflow
的属性值改为 hidden
的话,那么 text
的盒子就会建立 BFC。

这里我们就很明显的可以看见一个变化,
text
的元素整体作为一个 block level 的元素被排进了 BFC 里面。也就是说这个时候text
元素的宽度整个围绕着float-box
元素来进行排布了。
查看效果 | [查看代码](https://github.com/TriDiamond/frontend-tutorials/blob/master/css-tutorials/layouts/bfc-1.html) | 喜欢的同学 🌟star 一下谢谢!
BFC 合并与边距折叠
刚刚我们讲到, BFC 只会发生在同一个 BFC 里面。如果创建了新的 BFC 的话,它就不会发生边距折叠。如果没有创建 BFC 的话,它就存在着一个同向边距折叠。
好,同样我们用一段代码的实例来试验一下:
这里的两个
box
元素都给予了 100 x 100px 宽高,同时有一个 20px 的外边距。第二个box
元素我们用一个含有overflow
属性的div
包着。首先我们默认给予这个overflow
元素overflow: visible
,然后我们来看看会出现什么效果。

出来的效果显而易见,我们可以看到上面和下面的两个 box
元素的外边距发生了边距折叠
现象。这里两个盒子就当做 overflow
这个元素不存在一般,最后他们两个的距离就是 20px。
这里做一个小实现,如果我们给 overflow
元素加入一个 margin-top: 30px
,这个时候会对这两个 box
的距离发生影响吗?
答案是:会的!但是还是还是会出现
边距折叠
的现象,只是它们之间的距离变成了 30px,但是并不会变成 20+30+20 这样的效果。我们来看看运行效果:

最后如果我们把 overflow-box
的 overflow
属性改为 hidden
, 这个时候它就会创建 BFC。这时候我们就会发现,边距折叠
现象就会消失,最终两个 box
元素的距离就是 30+20 = 50px。


"不知道这里发生了什么,但是觉得很棒!哈哈"
这里的
overflow-box
里面的box
元素已经和外面的box
元素不在同一个 BFC 里面了。所以它们之间不会再发生边距折叠现象。但是 外面的overflow-box
和box
是处于同一个 BFC 中,所以它们两个依然会发生边距折叠现象。所以这个就是创建 BFC 对边距折叠这个行为的影响。

我是来自《技术银河》的三钻:
"学习是为了成长,成长是为了不退步。坚持才能成功,失败只是因为没有坚持。同学们加油哦!下期见!"

博主开始在 B 站每天直播学习,早上 06:00 点到晚上 11:30 。 欢迎过来《直播间》一起学习。
我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!
学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و

版权声明: 本文为 InfoQ 作者【三钻】的原创文章。
原文链接:【http://xie.infoq.cn/article/a2dc9eeae907abfa59048d280】。文章转载请联系作者。
评论