现在进入 CSS 的核心底层------"文档流(Normal Flow)"。
如果我们把 CSS 比喻为"排版的语言",
那么"文档流"就是它的语法基石 、世界物理法则 。
理解它,就能明白 CSS 世界中一切"位置、层次、空间"的根源。
初步认识
为什么会有"文档流"?------从排版问题说起
问题的起点:如何"自动排好内容"?
HTML 早期的目标是"结构化信息",而非"绘图"。
网页内容是一种可变信息流:
- 文字会换行;
- 图片大小不一;
- 浏览器窗口宽度也不固定。
💡 所以浏览器必须自己"推算出"元素该放哪。
于是它采用了一套最简单、最符合阅读逻辑的规则:
从上到下、从左到右地排列元素。
这套默认规则,就被称为------
👉 文档流(Normal Flow)。
文档流是什么?------网页的"空间分配算法"
我们可以把浏览器渲染过程想象成排版机器人:
1️⃣ 它读取 HTML,构建 DOM 树;
2️⃣ 它识别每个元素的类型(块、行内等),如果是块就垂直从上往下流动放置,如果是行内就水平从左往右流动放置;
3️⃣ 它按规则依次计算这些盒子的位置与大小。
于是,一个"流"被建立起来:
plain
┌─────────────────────────┐
│ │ Header(块)
├─────────────────────────┤
│ 文字(行内) 图片(行内) │ 段落(块)
└─────────────────────────┘
就像水流一样,元素一个接一个地"流动"与"堆叠"。
文档流并非属性,而是浏览器的默认排版机制。
所有布局系统(float、flex、grid、position)
都是对这条"流"的改造与重写。
文档流的两种流动方式(两大格式化上下文)
CSS 世界中,流并非单一,而是分为两种基本形态:
一种是块状排布,一种是行内排布。
格式化上下文 | 对应元素 | 排布方向 | 典型表现 |
---|---|---|---|
BFC(Block Formatting Context) | 块级元素(div、p、section) | 垂直方向从上到下 | 每个元素独占一行 |
IFC(Inline Formatting Context) | 行内元素(span、a、img) | 水平方向从左到右 | 内容自动换行对齐 |
块级布局(BFC)------"垂直堆叠的世界"
块级元素的默认特性
- 独占一行;
- 宽度自动撑满父容器;
- 高度由内容决定;
- 外边距可折叠(margin collapse)。
例:
plain
<div>第一个块</div>
<div>第二个块</div>
效果:
plain
第一个块
第二个块
浏览器的计算过程:
plain
1. 第一个div放到最上面;
2. 第二个div在第一个的下方;
3. 计算它们的margin、padding、border;
4. 排列出纵向序列。
🧠 这就是 Block Flow:从上到下的垂直流。
行内布局(IFC)------"文字流动的世界"
行内元素的默认特性
- 不换行;
- 只占内容宽度;
- 高度由行高控制;
- 受
line-height
、vertical-align
控制。
例:
plain
<p>这是 <span>行内文字</span> 排版</p>
效果(横向流):
plain
这是 行内文字 排版
浏览器的排版逻辑:
plain
1. 创建"行框(line box)";
2. 将每个行内盒(inline box)放入;
3. 超出宽度时换行,形成下一行;
4. 根据行高(line-height)对齐基线。
🧠 这就是 Inline Flow:从左到右的水平流。
现在我们初步了解了文档流的基本概念,下面为了真正理解什么是文档流,以及文档流的复杂应用(层级嵌套),我们需要理解文档流的设计思路与物理模型
文档流的物理模型------纵横联合到层级嵌套
排版的出发点:空间的分配与内容的对齐
我们先回到历史(1996年 CSS1 时代)看设计动机:
当时的网页结构是这样的:
plain
<body>
<h1>标题</h1>
<p>这是第一段文字。</p>
<p>这是第二段文字。</p>
</body>
目标:让网页像书页那样,
- 段落之间有上下间距(块级排列);
- 段落内部的文字能自动换行(行内排列)。
于是浏览器的设计者思考:
- 整个页面的**大框架(段落之间)**是"块与块之间"的垂直排列;
- 但每个段落内部是"行与行之间"的水平排列。
所以他们做了一个非常关键的层级分工设计:
层级 | 控制对象 | 负责的任务 | 对应概念 |
---|---|---|---|
外层(Block,块级上下文) | 各个"块"之间 | 控制整体空间分区、垂直排布 | "块流" |
内层(Inline,行内上下文) | 块内部的文字与小元素 | 控制行内内容、文字换行与对齐 | "行内流" |
于是,一个块级盒(如 <p>
)内部可以继续存在"行内上下文",来处理文字排版。
这就好比一本书的排版:
- 页与页之间的排布、边距控制 → 块级流
- 每一页中段落内文字、图片的排列 → 行内流
什么是流?
我们把整个页面想象成一堆"水流系统":
名称 | 类比 | 流动方向 | 排布对象 |
---|---|---|---|
块流(Block Flow) | 大水渠 | 垂直方向流动 | 一块块盒子 |
行内流(Inline Flow) | 小水流 | 水平方向流动 | 一行行文字或小图 |
- 块流(Block Flow) :像一条从上到下流动的主河道,每个块级盒子(如
<div>
、<p>
)是这条河道里的一块块"浮板",依次堆叠; - 行内流(Inline Flow):是某个浮板上流动的小溪水,它在"块"里面横着流,装着文字、图片等小内容。
所以整个 CSS 排版世界是:
"河道(块流)里装浮板(块盒),浮板上流小溪(行内流)。"
plain
Block Flow
├─ line box
│ ├─ inline box(文字、图片、链接)
│ └─ ...
└─ 下一个块
即:
块级元素负责"垂直排版"
行内元素负责"行内排版"
它们形成了一个完整的"流体体系":
- 外层控制方向与空间分配;
- 内层控制内容对齐与换行。
纵横联合
块流(Block Flow)
当浏览器渲染文档时,首先会创建一个"块级上下文"(Block Formatting Context,简称 BFC)。
在这个上下文中:
- 每个块级元素 (如
<div>
、<p>
、<section>
)都像一个"盒子"; - 它们从上到下依次排列;
- 每个盒子之间有间距(margin),不会互相重叠;
- 它们各自决定内部空间的"宽度"和"高度"范围。
简而言之,块流负责决定空间的外框结构。
例如:
plain
<div>
<p>第一段文字</p>
<p>第二段文字</p>
</div>
这时:
<div>
创建了一个块级上下文;<p>
元素作为两个块,从上到下排;- 每个
<p>
的宽度默认占满父级的宽度(称为"块级伸展")。
行内流(Inline Flow)
当我们进入某个块的内部,比如一个 <p>
,就进入了"行内格式化上下文"(Inline Formatting Context,简称 IFC)。
细心的同学读到这里一定会深感疑惑,为什么<p>
与<div>
同样作为块级元素,<div>
的内部就是BFC,而这里<p>
的内部就是IFC?这其实涉及到更深层次的一个设计,这里我们需要先了解行内流的含义,理解了基础,我们才可以更好地深入,下面我会带大家去理清缘由。
在这里:
- 所有文字、行内标签(如
<span>
、<a>
、<img>
)都在同一行内水平排列; - 如果一行放不下,就自动换行;
- 每一行被看作一个"行框"(line box);
- 每个字符或行内盒都位于行框的"基线"上对齐。
例如:
plain
<p>你好,<span>世界</span>!</p>
这里:
<p>
是块级盒;- 它的内部产生了若干行盒(line box);
- 每个行盒内包含若干 inline box(行内盒);
- 这些行内盒再由字符盒(glyph box)组成。
文档流树形结构
plain
文档流(Document Flow)
│
├── 块级格式化上下文(BFC) → 决定外层布局(垂直流动、盒子分布)
│ ├── 块盒(block box)...
│ └── 每个块内生成行内格式化上下文
│
└── 行内格式化上下文(IFC) → 决定内层排版(水平排列、基线对齐)
├── 行盒(line box)
│ ├── 行内盒(inline box)
│ │ └── 字形盒(glyph box)
│ └── 图片盒(img box)
└── 下一个行盒
块流解决"空间怎么划分",行内流解决"内容怎么对齐"。
外层负责"方向与空间分配",内层负责"文字与细节对齐"。
上面的各个盒子是什么意思呢?让我们继续往下看:
CSS 的"盒族系统"------文档流的基本构件
要理解真正的排版机制,必须知道每个"盒"是干什么的。
文档流中的盒子是分层的,就像一个嵌套的套娃:
plain
文档流(Document Flow)
│
├── 块级格式化上下文(BFC)
│ ├── 块盒(block box)
│ │ └── 行内格式化上下文(IFC)
│ │ ├── 行盒(line box)
│ │ │ ├── 行内盒(inline box)
│ │ │ │ ├── 字形盒(glyph box)
│ │ │ │ └── 图片盒(img box)
│ │ │ └── 下一个行内盒
│ │ └── ...
│ └── 下一个块
└── 下一个 BFC
我们依次拆开👇
块盒(Block Box)
- 由块级元素(
div
、p
、section
)生成; - 决定垂直方向的布局;
- 每个块盒形成一行(即"块流");
- 控制 margin 合并、宽度自适应、清除浮动等行为。
你可以理解为:
块盒 = 页面结构的"主干骨架"。
行盒(Line Box)
- 块盒中的文字或行内内容会被分割成"行";
- 每一行文字对应一个 line box;
- line box 的高度由该行中最高的元素决定。
举例:
plain
<p>Hello <strong>World</strong>!</p>
这里 <p>
生成一个块盒,内部的"Hello World!" 形成一个 行盒(line box)。
一行文字就是一个 line box。
行内盒(Inline Box)
- 每一个行内元素(如
<span>
、<a>
、em
)都会生成一个 inline box; - inline box 在行盒内按顺序水平排列;
- 它们的高度由字体行高(
line-height
)和对齐方式(vertical-align
)决定。
你可以理解为:
inline box 是"文字块"或"行中小容器"。
字形盒(Glyph Box)
- 每一个字符(字形)对应一个 glyph box;
- 它决定每个字的实际形状与占位;
- 它是浏览器排版的最底层单位(真正被绘制到屏幕上的)。
CSS 属性如 letter-spacing
、font-variant
、text-transform
都在 glyph 层面起作用。
图片盒(Image Box)
- 行内图片(
<img>
、inline SVG
)生成的盒; - 在 IFC(行内格式化上下文)中作为 inline-level box;
- 它也参与基线对齐、line-height 计算。
为什么要分这些盒?
这是因为 CSS 的排版逻辑是分层进行的:
层级 | 作用 | 举例 | 对应上下文 |
---|---|---|---|
Block Box | 控制垂直排列 | 段落、div、section | BFC |
Line Box | 控制每行内容 | 每一行文字 | IFC |
Inline Box | 控制行中元素 | span、a | IFC |
Glyph Box | 控制字形形态 | "A"、"你" | 字体引擎层 |
Image Box | 控制图片 | <img> |
IFC |
这就像打印机排字一样:
页面是由块组成的,
块中有行,
行中有行内内容,
行内内容由字与图组成。
关键认识:盒子的"包裹关系"
- 块盒(block box) 包裹一组 行盒(line box)
- 行盒(line box) 包裹若干 行内盒(inline box)
- 行内盒(inline box) 进一步包裹 字形盒 / 图片盒
即:
块流包裹行流,行流包裹字形流。
层级嵌套
盒子的阵营------格式化上下文**(Formatting Context)**
CSS 的排版机制规定了:
每个盒子都有"自身的格式化上下文 "(注意这个上下文不一定是由自己创造的,就像一个人属于某一阵营,但这个阵营不见得是自己建立的,也可能是加入了别人的阵营),并遵循就近支配原则。
关键分层:盒子 ≠ 格式化上下文
概念 | 本质 | 作用 |
---|---|---|
盒子(Box) | 元素生成的视觉容器(有宽高、边距等) | 放内容、显示样式 |
格式化上下文(Formatting Context) | 一种排版空间/规则系统 | 决定盒子如何排列 |
一句话理解:
盒子是"被排版的对象",
上下文是"排版的环境"。
谁创建谁?
不是每个盒子都会创建 一个新的上下文。
但每个盒子都属于某个上下文。
这就是关键。
举个类比:
"每个学生都属于某个班级,但不是每个学生都有自己一个人的班级。"
默认状态下:格式化上下文延续共享------没事用不着自立门户
我们用最简单的例子来说明:
plain
<div class="parent">
<p>文字1</p>
<p>文字2</p>
</div>
浏览器解析时:
1️⃣ <html>
创建根块级格式化上下文(root BFC) 。
2️⃣ .parent
是块盒(display: block
),它被放进根 BFC 中。
3️⃣ .parent
自己并不创建新 BFC (除非触发条件,如 overflow:hidden / float 等)。
4️⃣ .parent
的两个 <p>
也被放进同一个(根)BFC。
📊 结构图:
plain
BFC(root)
├── parent
│ ├── p
│ └── p
所以虽然"每个盒子都存在于某个格式化上下文中",
但它们共享同一个上下文实例 ,
也就是说------它们是在同一个空间规则下进行排列的。
何谓"就近支配原则"?
"就近支配"说的是:
盒子总是在"离自己最近的格式化上下文"中参与排版。
也就是说:
- 如果自己创建了新的上下文,就在自己的里面排;
- 如果自己没创建,就上交给父辈(最近的那个)去排。
比如:
plain
<div class="parent">
<div class="child"></div>
</div>
如果 .parent
没有触发新 BFC → .child
的排版就"上交"给上层(<html>
的 BFC)。
如果 .parent
触发了新 BFC → .child
就在 .parent
的上下文内排,不再上交。
总结对比表
盒子 | 是否创建新上下文 | 所属上下文 | 备注 |
---|---|---|---|
<html> |
✅ | 自己(根 BFC) | 文档根上下文 |
<div> 默认 |
❌ | 继承上层 BFC | 共享环境 |
<div> + overflow:hidden |
✅ | 自己(新 BFC) | 独立布局空间 |
<span> |
❌ | 父级块的行内格式化上下文 | IFC 内排版 |
关键理解:
所谓"每个盒子都有自己的格式化上下文",并不是指它一定创建一个 ,而是指它必然处于某个上下文之内。
因此:
- ✅ 语义一致:每个盒子都在上下文中;
- ✅ 逻辑一致:但默认情况下,它们共享上层上下文;
- ✅ 没有矛盾,只是"创建者"和"归属者"不同。
总结
关键点 | 实际含义 |
---|---|
"每个盒子都有自身的格式化上下文" | 每个盒子都被某个上下文支配(存在归属) |
"默认会共享上下文" | 只有触发条件的盒子才会新建上下文,其余都归入上层 |
"就近支配原则" | 排版由最近的上下文控制,不会跨层传导 |
从代码实现的角度理解格式化上下文
BFC / IFC / FFC 等格式化上下文,本质上是浏览器排版引擎内部创建的"对象实例"------一种可编程的数据结构。
也就是说,它们不是抽象理论 ,而是真正存在于浏览器的渲染树(Render Tree)中,有对应的数据对象、边界区域、布局算法函数。
从 HTML → DOM → Render Tree 的生成过程
让我们先回顾渲染的核心流程:
plain
HTML
↓ 解析
DOM(结构树)
↓ + CSS
Render Tree(渲染树)
↓
布局(Layout)
↓
绘制(Paint)
↓
合成(Composite)
👉 在"生成 Render Tree"这一步中,
浏览器会为每个元素创建一个 Layout Object(也叫 Frame、Box、Renderer,具体名称因引擎而异)。
每个 Layout Object 就是一个 盒模型实例 ,
它同时会被挂载到某个格式化上下文对象中。
BFC / IFC 在底层的真实形态
在 Chrome 的 Blink(以及早期 WebKit)中:
- 每个元素在渲染树中由一个
LayoutObject
表示; - 如果这个元素触发了一个新的 BFC,就会额外创建一个 "LayoutBlockFlow" 对象;
- 该对象有自己的坐标系、布局函数(
LayoutBlockFlow::layout()
); - 它内部再管理自己的子对象(这些子对象在此上下文内排版)。
伪代码结构如下:
javascript
class FormattingContext {
constructor(type, parent) {
this.type = type; // 'block' | 'inline' | 'flex' | ...
this.parent = parent; // 上级上下文
this.children = []; // 子盒子(boxes)
}
layout() {
if (this.type === 'block') {
this.blockLayout();
} else if (this.type === 'inline') {
this.inlineLayout();
}
// ...
}
blockLayout() {
// 垂直排列子盒子
let y = 0;
for (const child of this.children) {
child.position.y = y;
y += child.height + child.marginBottom;
}
}
inlineLayout() {
// 水平排列文字/行内盒
let x = 0;
for (const child of this.children) {
child.position.x = x;
x += child.width;
}
}
}
当浏览器解析:
plain
<div class="parent">
<p>文字1</p>
<p>文字2</p>
</div>
它大致会生成这样的对象层级:
javascript
rootBFC = new FormattingContext('block', null);
parentBox = { display: 'block', context: rootBFC };
p1Box = { display: 'block', context: rootBFC };
p2Box = { display: 'block', context: rootBFC };
rootBFC.children = [ parentBox ];
parentBox.children = [ p1Box, p2Box ];
因为 .parent
没有触发新的 BFC,
所以它与 <p>
元素共享同一个 rootBFC
实例。
也就是说------这些盒子都在同一个 FormattingContext
对象中执行同一套垂直布局逻辑。
当开启新 BFC 时会怎样?
当我们写:
plain
.parent {
overflow: hidden; /* 触发 BFC */
}
浏览器在构建渲染树时就会检测到这个条件,
于是:
javascript
rootBFC = new FormattingContext('block', null);
parentBFC = new FormattingContext('block', rootBFC);
parentBox.context = parentBFC;
p1Box.context = parentBFC;
p2Box.context = parentBFC;
rootBFC.children = [ parentBFC ];
parentBFC.children = [ p1Box, p2Box ];
📊 图形化理解:
plain
rootBFC
└── parentBFC
├── p1
└── p2
这样就"物理"地隔离了父外边距的传播路径。
也就是说------margin 塌陷不会跨 BFC 边界发生,
因为它们属于不同的上下文对象实例 ,
浏览器的布局计算函数不会合并它们的边界。
为什么说"共享同一个上下文实例"?
因为如果不触发新的上下文,所有兄弟盒子都:
- 属于同一个
FormattingContext
对象; - 使用同一个
layout()
函数; - 共享同一个坐标系(top 从上往下累加)。
因此,
当我们说"共享上下文"时,确实就是在说:
它们在底层共享同一个数据结构实例(一个 FormattingContext 对象)。
对比说明(带类比)
术语 | 概念层面 | 浏览器内部 | 类比现实 |
---|---|---|---|
盒子(Box) | 视觉矩形区域 | LayoutObject | 一张纸 |
格式化上下文(FC) | 布局规则空间 | FormattingContext 对象 | 一个书写的画布 |
创建 BFC | 独立布局空间 | 新建一个 FormattingContext 实例 | 在纸上开一块新区域独立写字 |
共享 BFC | 使用同一上下文 | 共享同一个 FormattingContext 实例 | 多张纸在同一画布上写字 |
总结
格式化上下文(BFC / IFC 等)是真实存在的对象实例,控制盒子的布局规则与坐标空间。
默认所有盒子共享上层上下文的实例,
当某元素触发新 BFC 时,浏览器会新建一个新的上下文对象,
从此内部的盒子在独立的空间中布局。
嵌套初识------元素的两重身份
- 元素既是「被排版的个体」,又是「排版他人的空间」。
- 也就是说:
- 它 一方面在父亲那里要排好自己,
- 另一方面又要 在自己体内排好孩子。
- 所以看似"创建上下文"与"共享上下文"矛盾,
其实是因为我们在谈"不同层级的语境"。
下面我们彻底把这件事讲清楚。
必须区分的两个层面
层面 | 问题 | 举例 |
---|---|---|
外层角色 | 它在父亲的世界里怎么活? | div 在父级中是一个块、span 是一个行内盒 |
内层世界 | 它自己如何安排孩子? | div 容纳块流或行内流,p 只容纳行内流 |
这两个层面正是前面你提到的:
① 在父级流中如何表现
② 在自己内部创建什么样的流
提前补充:CSS 的"双层 display 机制"
正是因为这种"内外分离"的思考,
后来CSS3 才提出了双层定义:
plain
display: <outer> <inner>;
例如:
写法 | 外层行为(在父中) | 内层行为(对子代) |
---|---|---|
display: block flow |
作为块盒出现 | 创建一个 块级格式化上下文(BFC) |
display: inline flow |
作为行内盒出现 | 创建一个 行内格式化上下文(IFC) |
display: block flex |
作为块盒出现 | 创建一个 Flex 格式化上下文(FFC) |
display: inline grid |
作为行内盒出现 | 创建一个 Grid 格式化上下文(GFC) |
几乎所有元素的默认 display
都同时定义了"在父中怎么表现"和"对儿子如何布局"。
后面我们会深入去讲一讲CSS 的"双层 display 机制",这里先有一个概念。
三种基本嵌套
-
块元素中只能直接形成块流 或行内流 (什么时候形成块流、什么时候形成行内流,与前面讲行内流时提到的
和
内部流差异的问题一样,先耐下心继续往下,逐步深入); -
行内元素的外层如果是块 → 它就在行内流中排列;
-
块中再嵌套块,会产生"新块流";
-
行内中嵌块,会触发"断流"或"匿名块盒"机制(流在大流里流,但不能反向穿透);
块级元素嵌套块级元素 ✅(允许)
plain
<div>
<p>一段文字</p>
<section>另一块内容</section>
</div>
逻辑:
外层 <div>
形成一个块流;
内部 <p>
、<section>
作为"块级盒",在垂直方向依次排列。
结果直观:
plain
[div]
├── [p]
└── [section]
这一层层结构就像:
- 一个主水渠里流着多个小浮板;
- 每个浮板之间有间隙(margin);
- 不冲突、不破坏主河道的流动。
块中嵌套行内元素 ✅(允许)
plain
<p>你好,<span>世界</span>!</p>
逻辑:
<p>
创建块流;- 块流内部自动形成行内格式化上下文(IFC);
- 所有文字与
<span>
都在这个 IFC 中水平排列; - 一行放不下,就自动换行。
可视化理解:
plain
Block Flow(垂直)
└─ Line Box(水平)
├─ Inline Box(文字"你好,")
├─ Inline Box(<span>世界</span>)
└─ Inline Box(文字"!")
就像一个大浮板上,放着一排排小积木(行内元素),顺序排开。
行内中嵌套块 ❌(不允许,断流)
plain
<p>这是一个段落,<div>我在中间放了个 div</div>看看会怎样?</p>
问题出现了:
<p>
本身是块级元素,内部是行内流;- 但
<div>
是块级盒; - 行内流里不允许出现块级盒,就像一条细水流里突然塞进一块大石头,会"堵住水流"!
浏览器怎么办?
它必须"改造流向"------于是会触发一种"断流机制",在渲染时:
<p>
是行内流;<div>
是块流;- 行内流中不能直接嵌块 → 浏览器会自动分裂
**<p>**
,将其重构 为三个匿名块:
plain
<p>这是一个段落,</p>
<div>我在中间放了个 div</div>
<p>看看会怎样?</p>
这就是所谓的 匿名块盒(anonymous block box)机制。
这体现了"文档流的层级自治":每层只能处理自己那一类排版。
通俗理解:
浏览器发现"细流中塞了大石头",就把细流一分为三段:
石头前一段小溪、石头本身、石头后一段小溪,
这样整个水流依然保持"自上而下"的结构。
嵌套规则总结
在文档流中:
每个盒子必须在自己所属的"流体层"中生效,不能越层。
规则 | 描述 | 举例 |
---|---|---|
块级盒只能存在于块级流中 | 块中可以嵌块 | <div><p></p></div> |
行内盒只能存在于行内流中 | 块中可以嵌行内 | <p><span></span></p> |
行内流中不能直接出现块盒 | 若出现则断流 | <p><div></div></p> |
每个盒子形成自己的上下文(重点) | 格式化上下文彼此独立 | BFC 、IFC 等 |
再看一层:为什么这么设计?
CSS 最初就是为"文本文档"设计的,而文本天然有"层次":
- 段落是大的逻辑块;
- 段落内部才有文字、图片;
- 文字中再嵌块,就会破坏"段落结构"。
所以浏览器必须维持"从外到内逐层独立"的格式化原则:
块控制空间分区,行内控制内容排列。
盒子的两个身份与格式化上下文
两个身份的表现
块级元素与行内元素,各自会形成各自的格式化上下文,去容纳其子元素,但同时,他们本身作为元素,也会成为其父元素的子元素,那么这两种不同身份,各自应当如何表现呢?
事实上,CSS 中,"元素"有两个"身份":
- 它在父级流中如何表现(自己是块盒还是行盒);
- 它在自己内部创建什么样的流(子内容如何排版)。
元素 | 在父级中的角色 | 自己内部的流 | 原理 |
---|---|---|---|
<div> |
块级盒(在父中垂直排列) | 块级格式化上下文(BFC) | 用于容纳更多块 |
<p> |
块级盒(在父中垂直排列) | 行内格式化上下文(IFC) | 用于排版文字 |
<ul> |
块级盒 | BFC(包含 <li> ) |
每个 li 自成块 |
<li> |
块级盒 | IFC(文字排列)或 BFC(嵌套列表) | 看内容类型 |
<span> |
行内盒 | 无(行内自身不再生成独立上下文) | 它只是 inline box |
很多人误以为"块级元素就一定在其内部产生块流",这是个误区。
正确的理解是:块级元素的"块性",只定义了它在父亲那一层的行为,而不是它内部的世界。
具体示例来分清"创建"与"共享"
例 1:div 中嵌套 div
plain
<div class="outer">
<div class="inner"></div>
</div>
元素 | 外层行为 | 内层规则 | 结果 |
---|---|---|---|
outer | 在根 BFC 中垂直排列 | 内部定义 block flow(BFC) | 内部布局规则为 block flow,但仍属于根 BFC |
inner | 在 outer 的 block flow 中垂直排列 | 内部同样是 block flow | 嵌套不隔离 margin、float 等影响 |
解释:
inner 处在 outer 的排版空间中,
虽然 inner 自己也"定义了 BFC 规则",
但并没"触发独立 BFC",
它的流与 outer 的是连续的。
例 2:p 中嵌文字与 span
plain
<p>Hello <span>world</span></p>
元素 | 外层行为 | 内层规则 | 结果 |
---|---|---|---|
p | 块级盒,在根 BFC 中垂直排列 | 内部是 IFC(行内流) | 内容横向排列、文字换行 |
span | 行内盒,在 p 的 IFC 中排列 | 不创建新流 | 参与 p 的行盒排版 |
解释:
p
在外层是块(block),但它的内部不是块流,而是行内流(inline flow),
这就是"块中包行"的层次结构。
例 3:触发独立上下文
plain
.outer {
overflow: hidden; /* 触发独立 BFC */
}
此时:
.outer
不再与根 BFC 共享;- 它创建一个真正隔离的"排版空间";
- 子元素 float、margin 不会影响外部。
这才是"独立的格式化上下文(new context)"。
总结核心逻辑图
plain
每个元素:
├─ 外层身份:作为子参与父的上下文(共享或隔离)
└─ 内层身份:作为父支配自己的子(定义排版规则)
📘 统一认知:
- "创建格式化上下文" = 定义内部布局规则;
- "共享格式化上下文" = 没有触发新的独立空间;
- 它们并不矛盾,是不同层级的逻辑。
<div>
与<p>
内部流差异的问题:第二身份摇摆不定------我是底下的盒子是跟我一起在BFC的这大家庭,还是我成立IFC去管他们?
谁决定"内部流类型"?
这是浏览器渲染排版的一个底层算法:
每个盒子在渲染时会经历:
- 确定自己的外部表现形式(display 的外层部分);
- 根据内部内容类型创建对应的"格式化上下文"(Formatting Context)。
这个过程由**HTML层的内容语义模型(Content Model)和CSS层的布局模型(Display Model)**共同决定。
display 的双层模型
这里我们再次来看display 的双层模型 (CSS3 才正式明确语法,但其理念自CSS1就确定并应用)这个概念,方便我们理解CSS布局模型:
其实 display
包含两层含义:
plain
display: <outer> <inner>;
| 外层(outer) | 控制它在父级中如何参与排版 | 例如 block / inline / list-item |
| 内层(inner) | 控制它内部内容如何排版 | 例如 flow / flex / grid / ruby |
完整写法 | 含义 |
---|---|
display: block flow; |
外层表现为块级,内部使用普通文档流排版(flow layout) |
display: inline flow; |
外层表现为行内盒,内部也是普通流 |
display: block flex; |
外层为块级,内部为弹性布局 |
display: block grid; |
外层为块级,内部为网格布局 |
其中:
flow
就代表传统的"普通文档流布局(Normal Flow)"。
它包含两种内部上下文:
- 块格式化上下文(BFC,Block Formatting Context)
- 行内格式化上下文(IFC,Inline Formatting Context)
二者的CSS布局模型
plain
div { display: block flow; }
p { display: block flow; }
对于{ display: block flow; }
,浏览器的算法规则是:
若块盒内部内容为文本或行内级元素 → 创建 IFC;
若块盒内部内容为块级元素 → 创建 BFC。
<div>
虽然是块级,但它里面是块(div/p等),所以内部形成块流;<p>
虽然是块级,但它里面是文字(行内),所以内部形成行内流。
但注意,只是初步解释,因为我们前面说是**HTML层的内容语义模型(Content Model)和 CSS层的布局模型(Display Model)**二者共同决定内部流类型,我们现在只看了CSS层的布局模型,二者都是{ display: block flow; }
,也就是说内层是默认文档流,会根据内容是什么自行决定变成BFC还是IFC,但为什么明明都是flow,
的内部却不允许放块级元素呢?
CSS 只是"表现层",而 HTML 决定了"能不能这么做"
CSS 的 display
属性负责描述"如何展示";
而 HTML 的标签本身,决定"能包含什么内容"。
这两者的关系是:
层级 | 谁控制 | 控制什么 |
---|---|---|
HTML 层 | 内容语义模型(Content Model) | 哪些标签可以嵌哪些标签 |
CSS 层 | 布局模型(Display Model) | 嵌进去后,怎么排版 |
举例:
plain
<p>这是段落 <span>行内</span>。</p> ✅ 合法
<p>这是段落 <div>块级</div>。</p> ❌ 非法
- 从 CSS 的角度 :
p
是display: block flow;
,理论上内部可以排版块或行内。 - 但从 HTML 的角度 :
p
的内容模型(content model)规定:
"p
只能包含_短语内容(phrasing content)_,即行内内容。"
这就是标准强制的规则:
<p>
元素不是结构容器,而是语义化的文本段落 。
它代表一个行内排版区域(IFC 容器),内部不允许出现块级元素。
因此:
<div>
:结构性容器,无语义限制,能容纳块或行内;<p>
:语义性容器,HTML 层面限制只能放文字或行内标签。
👉 ** 问题的根源不是 CSS display,而是 HTML 语义规范。
**<p>**
不能容纳块,是"语义限制",不是"排版能力不足"。 **
那为什么 <div>
没有限制?
因为 <div>
的语义是:
"通用分区容器(Generic Block Container)"
即:它没有自己的语义,只负责结构。
因此浏览器在构建渲染树时:
- 发现
<div>
是块级; - 发现内部是什么都行;
- 就直接根据内部内容决定上下文类型。
<div> 内部内容 |
形成的上下文 | 原理 |
---|---|---|
文本 / 行内元素 | 行内格式化上下文(IFC) | 像段落一样换行排版 |
块级元素 | 块格式化上下文(BFC) | 垂直排列 |
所以 <div>
是个"中立容器",能容纳任何类型的流。
从HTML、CSS、浏览器"职责分离"的角度再看
层次 | <div> |
<p> |
说明 |
---|---|---|---|
HTML 内容模型 | 任意内容(块/行内) | 仅短语内容(行内) | 决定嵌套合法性 |
CSS display | block flow | block flow | 排版潜能一致 |
浏览器渲染时的行为 | 根据内容生成 BFC 或 IFC | 强制创建 IFC,仅允许行内流 | 实际呈现不同 |
HTML 决定能不能这么排,CSS 决定排出来是什么样,浏览器负责解读两者并呈现。
总结
<div>
是块级容器,内容是块流;<p>
是块级段落,内容是行内流;
它们都"是块",但职责不同:
前者管理空间(外层结构),后者管理文字(内层排版)。
想象网页是一个办公大楼:
元素 | 角色 | 含义 |
---|---|---|
<div> |
通用办公楼 | 你可以在里面放办公室、仓库、会议室(块),或办公桌(行内) |
<p> |
专用办公区(写字间) | 只能放办公桌(文字),不能再盖会议室(块) |
所以:
div
是"可扩展楼层";p
是"专用文字层"。
BFC 与 IFC 的层级关系与触发机制
前置思考
对于:<div><p>Hello <span>world</span></p></div>
div共享了顶层的BFC,p也共享了顶层的BFC,但是p的内部,是IFC,这里的IFC是不是新的独立的格式化上下文?这里没有涉及到overflow: hidden;这样的触发独立上下文的语句
先看结构与默认上下文
HTML:
plain
<div>
<p>Hello <span>world</span></p>
</div>
默认情况下,这个结构中存在以下层级:
元素 | 格式化上下文类型 | 是否独立 |
---|---|---|
<div> |
BFC(Block Formatting Context,块级格式化上下文) | ❌ 不独立(延用顶层) |
<p> |
参与父级 BFC 的一个块级盒子 | ❌ 不独立 |
<p> 内部的文字与 <span> |
IFC(Inline Formatting Context,行内格式化上下文) | ✅ 独立(嵌套于 BFC 中) |
关键结论:IFC 是新上下文,但不是"块级意义上的独立上下文"
我们思考的重点是:
"p 的内部是 IFC,这是不是新的独立的格式化上下文?明明没触发 overflow:hidden 等属性呀?"
👉 答案是:是的,它是新的格式化上下文(IFC),但它和 BFC 属于不同类型的上下文。
- BFC (Block Formatting Context)负责块级盒子之间的排列与外边距折叠等;
- IFC (Inline Formatting Context)负责行内盒子(文字、inline元素)之间的排版、行盒计算。
两者之间不是并列关系,而是嵌套关系:
一个块级盒子(例如 <p>
)在参与父级的 BFC 布局时,其内容区域内部会自动生成一个 IFC,用于布局文本与行内元素。
IFC 是如何自动产生的?
触发 IFC 不需要写 overflow: hidden
、display: inline-block
等属性。
触发条件非常简单:
当一个盒子包含行内级内容(文字、<span>
、<a>
等),则会自动创建一个"行内格式化上下文(IFC)"来排列这些内容。
也就是说:
plain
<p>Hello <span>world</span></p>
中,<p>
内部自动进入了 IFC 模式。
这时:
- "Hello " 是一个匿名行内盒;
<span>
是一个行内盒;- 它们一起被 IFC 管理,形成"行盒(line box)";
<p>
的高度由这些行盒决定。
总结:BFC 与 IFC 的嵌套关系(类比图)
可以这么想:
plain
┌────────────────────┐
│ div(BFC 容器) │ ← 管理块级盒子排列
│ ┌────────────────┐ │
│ │ p(块盒) │ │ ← 参与 div 的 BFC
│ │ ┌────────────┐ │ │
│ │ │ Hello <span> │ │ │ ← p 内自动形成 IFC(行内格式化上下文)
│ │ └────────────┘ │ │
│ └────────────────┘ │
└────────────────────┘
所以:
<div>
创建了一个 BFC;<p>
是这个 BFC 的子块;<p>
的内部自动形成 IFC,独立负责内部的文字与行内布局;- 这两个上下文独立负责不同维度的排列,不相互干扰。
为什么这种嵌套很重要?
它解释了许多 CSS 现象,例如:
- 文本排版不受外层 margin 折叠影响
因为文字处于 IFC 内部,而 margin 折叠只在 BFC 层级之间发生。 **vertical-align**
** 只能在 IFC 内起作用**
因为它是行内盒子的相对对齐,而 BFC 不理解 vertical-align。- 行高(line-height)计算在 IFC 中进行
因为每个行盒的高度由 IFC 决定,而不是 BFC。
具体我们会在下面逐个深入。
简要总结
概念 | 全称 | 管理对象 | 触发条件 | 是否独立上下文 |
---|---|---|---|---|
BFC | Block Formatting Context | 块级盒子之间的布局 | float 、overflow 、display: flow-root 等 |
✅ |
IFC | Inline Formatting Context | 行内元素、文本的布局 | 盒子中出现行内级内容 | ✅(独立于 BFC 内部) |
BFC 管"块的排列",IFC 管"字的排列"。
块中有字,就会自动生出 IFC;
字的世界不需 overflow:hidden 才独立。
BFC与IFC之间的差异
总体认知先行:块与字的"两界之分"
在 CSS 的视觉格式化模型中,所有元素都要落入某种"格式化上下文(Formatting Context)"中:
- 块格式化上下文(BFC) :管理块级盒子的布局(上下排列、margin 折叠、浮动避让......)
- 行内格式化上下文(IFC):管理行内盒子(文字、inline 元素)的布局(行盒高度、基线、vertical-align......)
一句话概括:
BFC 决定"段落与段落"如何排,
IFC 决定"字与字"如何排。
文本排版不受外层 margin 折叠影响
因为文字处于 IFC 内部,而 margin 折叠只在 BFC 层级之间发生。
1. 问题场景
看这样一段代码:
plain
<div>
<p>Hello</p>
<p>World</p>
</div>
默认情况下:
<div>
创建了一个 BFC;<p>
是块级盒,参与这个 BFC;- 每个
<p>
自身内部又是一个 IFC,用来排字。
2. Margin 折叠发生在哪里?
margin 折叠(Margin Collapsing) 的特征是:
只发生在"块级盒子"之间的 垂直方向。
比如:
- 相邻两个块(
p
与p
)之间的margin-bottom
与margin-top
会折叠; - 块的第一个子块的
margin-top
会与父块的margin-top
折叠。
但注意:
- 行内元素(inline)没有 margin 折叠机制;
- IFC 内部的所有计算,都不参与 BFC 的 margin 折叠规则。
3. 为什么文字不会被折叠?
因为文字、<span>
等都处在 IFC 的行盒体系中。
- IFC 管理的是行盒(line box) ,它们的上下间距由
line-height
和字体盒模型决定; - 这些行盒并不是"块级盒",所以 margin 折叠规则根本不会触发。
4. 结论
Margin 折叠 是块与块之间的"垂直世界"规则;
文字布局 属于 IFC 的"水平世界";
两个世界的规则互不干扰。
所以文字(IFC 内)永远不受外层 BFC 的 margin 折叠影响。
vertical-align
只能在 IFC 内起作用
因为它是行内盒子的相对对齐,而 BFC 不理解 vertical-align。
1. 问题场景
plain
<p>
文本 <img src="..." alt="" style="vertical-align: middle;">
</p>
我们用 vertical-align: middle
想让图片与文字垂直居中。
但如果你写在一个块上,比如:
plain
div {
vertical-align: middle;
}
就完全无效。为什么?
2. 关键原因:vertical-align 属于 IFC 的语义
vertical-align
的语义是:
控制"行内盒"在当前 行盒(line box) 中相对于基线(baseline)的垂直对齐方式。
换句话说:
- 它只存在于 IFC 内的行盒层面;
- 每个行内元素都会被放在某个行盒中,CSS 会根据 baseline、上升线(ascent)、下降线(descent)等字体度量值计算垂直对齐。
BFC(块级世界)中没有这些概念,它的垂直布局由 margin、border、padding、height 等决定。
因此 BFC 根本"不理解 vertical-align 这句话的语义"。
3. 结论
vertical-align
是 IFC 的语言,BFC 听不懂。
它作用于行盒内的元素,而非块级元素。
行高(line-height)计算在 IFC 中进行
因为每个行盒的高度由 IFC 决定,而不是 BFC。
1. 关键点:行高的本质是什么?
当一个块级盒(如 <p>
)中有行内内容时,浏览器会为它创建 IFC,并在其中生成一系列 行盒(line box)。
每一行的高度(line box height)就是我们常说的 line-height
的体现。
2. 行盒是怎么计算的?
行盒高度来源于每个行内盒的尺寸。
例如:
- 文本节点 → 字体的
ascent + descent
<img>
→ 图片高度<span style="font-size:2em">
→ 放大后的字体度量
浏览器会取这些值的"最大上升线 + 最大下降线"作为行盒高度。
line-height
会用来调整行盒的上下间距。
当多个行盒垂直堆叠时,line-height
决定它们的距离。
3. 为什么不由 BFC 管?
因为:
- BFC 管的是块与块的距离;
- IFC 内的行盒是"块内的内部细节",由 IFC 自己排布;
- 所以 BFC 只看到
<p>
整体的高度,不参与行高计算。
4. 一句话总结
行高是 IFC 内部的"微观排版属性",
BFC 只关心块整体的"宏观空间尺寸"。
BFC 与 IFC 的分工协作
领域 | 参与对象 | 控制内容 | 代表属性 | 折叠或对齐规则 |
---|---|---|---|---|
BFC(块世界) | 块级盒子(div , p , section ) |
上下排列、margin 折叠、float 清除 | margin , overflow , display , position |
margin 折叠 |
IFC(字世界) | 行内盒子(文字、span , img ) |
行内排版、基线对齐、行高控制 | line-height , vertical-align |
无折叠,按基线对齐 |
BFC 决定"块之间怎么排";
IFC 决定"块里的字怎么排"。
Margin 折叠是块世界的规则,
Vertical-align 与 Line-height 是字世界的语言。
display属性发展史
我们今天习惯写的 display:flex;
、display:block;
、display:inline-block;
等,其实是对 CSS 两层模型的一种"简化封装"。
要真正理解这一点,我们得从历史演化角度来讲起。
CSS1 时代(1996):display
只有"外层含义"
当年,CSS 还非常简单,网页的结构也非常单一。
当时的目标
------只是让"块"和"行"排得整齐。
于是,display
的设计也非常朴素,只描述了外层的排布方式(outer display type),即:
plain
display: block; /* 独占一行,垂直排列 */
display: inline; /* 与文字一起排,水平排列 */
display: none; /* 不显示 */
这时的"内层内容"根本没被考虑到,因为:
- 那时的网页内容主要是文字;
<p>
里的文字都是纯文本,不存在"复杂子结构";- 没有表格、没有浮动、没有定位。
所以早期的浏览器默认认为:
"块元素内部就是一堆行(inline context)。"
因此:
plain
<p>文字文字文字</p>
👉 <p>
自己是"块流(block)",
但它的内部内容是"行内流(inline)"。
这就是为什么:
p 是块级元素,却只能排行内内容。
CSS2 时代(1998):引入"双层 display 模型"
网页变复杂了,人们开始需要控制表格、浮动、列表、弹性排布。
W3C 于是重新定义了 display
:
display = <outer-display>
+ <inner-display>
即:
plain
display: block flow;
display: inline flow;
display: block flex;
display: block grid;
display: inline flex;
但问题是:
CSS2 时期的语法规范中,并没有支持写两部分!
所以虽然在规范内部是分开的(逻辑上),但写法上仍然只能写一个关键词,浏览器会自动推导出另一半。
例如:
写法 | 实际对应的两层 |
---|---|
display: block; |
block flow |
display: inline; |
inline flow |
display: flex; |
block flex |
display: inline-flex; |
inline flex |
display: grid; |
block grid |
display: inline-grid; |
inline grid |
display: table; |
block table |
也就是说:
你写一个词,浏览器会自动"推断出"另一层含义。
为什么要分两层?
这是因为------CSS 世界是嵌套的。
每个盒子既有外在行为 ,又有内部规律:
层级 | 控制对象 | 举例 | 类比 |
---|---|---|---|
外层 display | 元素在外部文档流中怎么"占位" | block / inline | 一个人"在社会上的身份" |
内层 display | 元素内部的内容怎么排版 | flow / flex / grid | 一个人"在家里的生活方式" |
举例:
plain
display: block flex;
表示:
- 这个盒子在外面表现为一个"块"(独占一行);
- 但它内部用"弹性布局"排列子项。
为什么 CSS 只写一个值?
因为:
- 历史兼容性------早期浏览器(CSS1、CSS2)根本不识别双值;
- 用户友好性------写一个词就够了,浏览器帮你补;
- 简洁直观------大多数开发者并不关心内外层的细节;
- 可扩展性------CSS3 之后,标准仍保留了双层结构,只是"语法层面上简化"。
在现代 CSS 中,双层写法重新被正式支持
CSS Display Level 3(2020s)终于允许我们显式写双值:
plain
display: block flow;
display: block flex;
display: inline flow-root;
display: block grid;
这样写是完全合法的。
而且还引入了新的内层类型:
flow-root
:开启新的块格式化上下文(相当于overflow:hidden
hack)contents
:让元素本身消失,只保留子元素结构table
/ruby
:用于表格或注音排版
演化对照表
时代 | 写法 | 语义层数 | 浏览器处理逻辑 | 举例 |
---|---|---|---|---|
CSS1 | display:block; |
单层(outer) | 默认内层为 flow | 简单文字排版 |
CSS2 | display:block; |
双层(outer+inner) | 隐式推断 inner | 块流中嵌行流 |
CSS3 | display:flex; |
双层(outer+inner) | outer=block, inner=flex | 弹性盒布局 |
CSS4+ | display:block flex; |
双层(显式) | 明确两层含义 | 可定制排版模型 |
补充:display: inline-block
的双层结构与含义
先看它的标准定义:
plain
display: inline-block;
这是一个复合 display 类型,展开后其实是:
plain
display: inline flow-root;
outer display:inline
表示这个盒子在外部的行为方式。
- 它 像文字一样 在行内排布;
- 会与文字或其他 inline 元素在同一行中并排出现;
- 受到基线(baseline)对齐的影响。
换句话说,它不是独占一行,而是一个"能进文字队列"的盒子。
inner display:flow-root
表示它内部的内容如何排布。
- 它内部是一个独立的块级格式化上下文(BFC);
- 可以在内部放任何块元素;
- 内部不会与外部流相互影响(不会被浮动塌陷或 margin 合并干扰)。
因此:
🔸 它"在外面看起来像个字",
🔸 但"里面却是一个小的独立世界,可以正常排版块内容"。
小结
层级 | 值 | 含义 |
---|---|---|
outer display | inline | 像文字一样并排 |
inner display | flow-root | 内部自成一格,像小型 div |
所以 inline-block
就像一个微型独立排版岛:
"能插入句子里的一整个小盒子。"
这实际上就解决了我们前面提到的行内流中不能插入块流的问题!
举例说明:
plain
文字 <div style="display:inline-block;">A<br>B<br>C</div> 文字
视觉效果:
plain
A
B
文字 C 文字
说明:
- 这个
div
在外部被当作文字(inline); - 但内部有多行块内容(flow-root),自己排自己的,不影响外层。
继文档流后的发展
脱离文档流:浮动与定位的突破
文档流虽然稳定,但有时太"死板"。
于是后来人类为它开辟了逃脱机制:
技术 | 意义 | 结果 |
---|---|---|
float | 元素"漂浮"在流之上 | 文本环绕效果 |
position: absolute | 元素脱离父流,独立定位 | 精准放置 |
position: fixed | 相对视口固定 | 悬浮导航等 |
flex / grid | 建立新的流(新格式化上下文) | 容器级布局 |
它们都在"改变文档流的力量分配方式"。
文档流的物理隐喻
可以这样理解整个排版宇宙:
隐喻 | 意义 |
---|---|
"文档流" 是引力场 | 决定元素自然落地的位置 |
"浮动 / 定位" 是外力 | 改变元素的自然轨迹 |
"格式化上下文" 是局部时空 | 控制该区域内的物理规则 |
"Flex / Grid" 是新文明 | 重新定义流的维度与秩序 |
从"流"理解一切布局
1️⃣ 当你看一个布局,不要问"元素为什么在那里",
而要问"它是在哪个流中?"
2️⃣ 判断规则:
- 它是否在文档流中?
- 它创建了新的格式化上下文吗?
- 它如何与父流交互?
3️⃣ 真正掌握 CSS 的关键是:
学会"观察流动"而不是"记忆属性"。
核心总结
概念 | 定义 | 控制方向 | 是否脱离流 |
---|---|---|---|
文档流(Normal Flow) | 默认的排版机制 | 上下 + 左右 | 否 |
BFC(块格式化上下文) | 块级排列环境 | 垂直流 | 否 |
IFC(行内格式化上下文) | 行内排列环境 | 水平流 | 否 |