CSS排版布局篇(2):文档流(Normal Flow)

134 阅读42分钟

现在进入 CSS 的核心底层——“文档流(Normal Flow)”。

如果我们把 CSS 比喻为“排版的语言”,
那么“文档流”就是它的语法基石世界物理法则
理解它,就能明白 CSS 世界中一切“位置、层次、空间”的根源。


初步认识

为什么会有“文档流”?——从排版问题说起

问题的起点:如何“自动排好内容”?

HTML 早期的目标是“结构化信息”,而非“绘图”。
网页内容是一种可变信息流

  • 文字会换行;
  • 图片大小不一;
  • 浏览器窗口宽度也不固定。

💡 所以浏览器必须自己“推算出”元素该放哪。
于是它采用了一套最简单、最符合阅读逻辑的规则:

从上到下、从左到右地排列元素。

这套默认规则,就被称为——
👉 文档流(Normal Flow)


文档流是什么?——网页的“空间分配算法”

我们可以把浏览器渲染过程想象成排版机器人:

1️⃣ 它读取 HTML,构建 DOM 树;
2️⃣ 它识别每个元素的类型(块、行内等),如果是块就垂直从上往下流动放置,如果是行内就水平从左往右流动放置;
3️⃣ 它按规则依次计算这些盒子的位置与大小。

于是,一个“流”被建立起来:

┌─────────────────────────┐
│                         │   Header(块)
├─────────────────────────┤
│ 文字(行内) 图片(行内) │   段落(块)
└─────────────────────────┘

就像水流一样,元素一个接一个地“流动”与“堆叠”

文档流并非属性,而是浏览器的默认排版机制。
所有布局系统(float、flex、grid、position)
都是对这条“流”的改造与重写。


文档流的两种流动方式(两大格式化上下文)

CSS 世界中,流并非单一,而是分为两种基本形态,即文档流的组成

  • 块流(Block Flow)
  • 行内流(Inline Flow)
流动方式对应元素排布方向典型表现
块流(Block Flow)块级元素(div、p、section)垂直方向从上到下每个元素独占一行
行内流(Inline Flow)行内元素(span、a、img)水平方向从左到右内容自动换行对齐

这里我们提前引入两个概念——块级格式化上下文BFC和行内格式化上下文IFC,以方便我们后续讲解引用。

格式化上下文对应流动方式意义
BFC(Block Formatting Context)块流(Block Flow)限定了块流的流动边界
IFC(Inline Formatting Context)行内流(Inline Flow)限定了行内流的流动边界
  • 原本的块流和行内流,只提到流,但流到哪里去呢?没有范围限定,那默认的范围就是根盒子,即最顶层的盒子,在最顶层的盒子里,随便流动,但总不能流动到最顶层盒子的外面,否则就看不到了,那就没有意义了。
  • 而BFC和IFC的出现,就是除了原本最顶层盒子的边界,我们可以在内部的盒子里,再定义创建一个流动边界。
  • 对于BFC和IFC的解释先到这里,后面随着案例的逐步增加,会反复地提到BFC与IFC,并做进一步解说。

块流(Block Flow)——“垂直堆叠的世界”

块级元素的默认特性

  • 独占一行;
  • 宽度自动撑满父容器;
  • 高度由内容决定;
  • 外边距可折叠(margin collapse)。

例:

<div>第一个块</div>
<div>第二个块</div>

效果:

第一个块
第二个块

浏览器的计算过程:

1. 第一个div放到最上面;
2. 第二个div在第一个的下方;
3. 计算它们的margin、padding、border;
4. 排列出纵向序列。

🧠 这就是 Block Flow:从上到下的垂直流。


行内流(Inline Flow)——“文字流动的世界”

行内元素的默认特性

  • 不换行;
  • 只占内容宽度;
  • 高度由行高控制;
  • line-heightvertical-align 控制。

例:

<p>这是 <span>行内文字</span> 排版</p>

效果(横向流):

这是 行内文字 排版

浏览器的排版逻辑:

1. 创建“行框(line box)”;
2. 将每个行内盒(inline box)放入;
3. 超出宽度时换行,形成下一行;
4. 根据行高(line-height)对齐基线。

🧠 这就是 Inline Flow:从左到右的水平流。


注意:手动创建独立格式化上下文BFC和IFC,是自CSS2的float出现后,在CSS2.1才实现的(但概念早早就有,只是不能手动创建,只能共享根盒子的BFC),为的是将网页的各个盒子容器化,让不同盒子的内部空间彼此隔离。我们文档流中就讲到这两个概念,是为了方便引用讲解,建立直观的形象。

现在我们初步了解了文档流的基本概念,下面为了真正理解什么是文档流,以及文档流的复杂应用(层级嵌套),我们需要理解文档流的设计思路与物理模型


文档流的物理模型——纵横联合到层级嵌套

排版的出发点:空间的分配与内容的对齐

我们先回到历史(1996年 CSS1 时代)看设计动机:

当时的网页结构是这样的:

<body>
  <h1>标题</h1>
  <p>这是第一段文字。</p>
  <p>这是第二段文字。</p>
</body>

目标:让网页像书页那样,

  • 段落之间有上下间距(块级排列);
  • 段落内部的文字能自动换行(行内排列)。

于是浏览器的设计者思考:

  • 整个页面的**大框架(段落之间)**是“块与块之间”的垂直排列;
  • 每个段落内部是“行与行之间”的水平排列。

所以他们做了一个非常关键的层级分工设计

层级控制对象负责的任务对应概念
外层(Block,块级上下文)各个“块”之间控制整体空间分区、垂直排布“块流”
内层(Inline,行内上下文)块内部的文字与小元素控制行内内容、文字换行与对齐“行内流”

于是,一个块级盒(如 <p>)内部可以继续存在“行内上下文”,来处理文字排版。

这就好比一本书的排版:

  • 页与页之间的排布、边距控制 → 块级流
  • 每一页中段落内文字、图片的排列 → 行内流

什么是流?

我们把整个页面想象成一堆“水流系统”:

名称类比流动方向排布对象
块流(Block Flow)大水渠垂直方向流动一块块盒子
行内流(Inline Flow)小水流水平方向流动一行行文字或小图
  • 块流(Block Flow):像一条从上到下流动的主河道,每个块级盒子(如 <div><p>)是这条河道里的一块块“浮板”,依次堆叠;
  • 行内流(Inline Flow):是某个浮板上流动的小溪水,它在“块”里面横着流,装着文字、图片等小内容。

所以整个 CSS 排版世界是:

“河道(块流)里装浮板(块盒),浮板上流小溪(行内流)。”

Block Flow
 ├─ line box
 │   ├─ inline box(文字、图片、链接)
 │   └─ ...
 └─ 下一个块

即:

块级元素负责“垂直排版”
行内元素负责“行内排版”

它们形成了一个完整的“流体体系”:

  • 外层控制方向与空间分配;
  • 内层控制内容对齐与换行。

纵横联合

块流(Block Flow)

当浏览器渲染文档时,首先会创建一个“块级上下文”(Block Formatting Context,简称 BFC)。

在这个上下文中:

  • 每个块级元素(如 <div><p><section>)都像一个“盒子”;
  • 它们从上到下依次排列;
  • 每个盒子之间有间距(margin),不会互相重叠;
  • 它们各自决定内部空间的“宽度”和“高度”范围。

简而言之,块流负责决定空间的外框结构。

例如:

<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);
  • 每个字符或行内盒都位于行框的“基线”上对齐。

例如:

<p>你好,<span>世界</span>!</p>

这里:

  • <p> 是块级盒;
  • 它的内部产生了若干行盒(line box)
  • 每个行盒内包含若干 inline box(行内盒)
  • 这些行内盒再由字符盒(glyph box)组成。

文档流树形结构
文档流(Document Flow)
│
├── 块级格式化上下文(BFC) → 决定外层布局(垂直流动、盒子分布)
│     ├── 块盒(block box)...
│     └── 每个块内生成行内格式化上下文
│
└── 行内格式化上下文(IFC) → 决定内层排版(水平排列、基线对齐)
      ├── 行盒(line box)
      │    ├── 行内盒(inline box)
      │    │    └── 字形盒(glyph box)
      │    └── 图片盒(img box)
      └── 下一个行盒

块流解决“空间怎么划分”,行内流解决“内容怎么对齐”。
外层负责“方向与空间分配”,内层负责“文字与细节对齐”。

上面的各个盒子是什么意思呢?让我们继续往下看:


CSS 的“盒族系统”——文档流的基本构件

要理解真正的排版机制,必须知道每个“盒”是干什么的。
文档流中的盒子是分层的,就像一个嵌套的套娃:

文档流(Document Flow)
│
├── 块级格式化上下文(BFC)
│     ├── 块盒(block box)
│     │     └── 行内格式化上下文(IFC)
│     │            ├── 行盒(line box)
│     │            │     ├── 行内盒(inline box)
│     │            │     │     ├── 字形盒(glyph box)
│     │            │     │     └── 图片盒(img box)
│     │            │     └── 下一个行内盒
│     │            └── ...
│     └── 下一个块
└── 下一个 BFC

我们依次拆开👇


块盒(Block Box)
  • 由块级元素(divpsection)生成;
  • 决定垂直方向的布局
  • 每个块盒形成一行(即“块流”);
  • 控制 margin 合并、宽度自适应、清除浮动等行为。

你可以理解为:

块盒 = 页面结构的“主干骨架”。


行盒(Line Box)
  • 块盒中的文字或行内内容会被分割成“行”;
  • 每一行文字对应一个 line box;
  • line box 的高度由该行中最高的元素决定。

举例:

<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-spacingfont-varianttext-transform
都在 glyph 层面起作用。


图片盒(Image Box)
  • 行内图片(<img>inline SVG)生成的盒;
  • 在 IFC(行内格式化上下文)中作为 inline-level box;
  • 它也参与基线对齐、line-height 计算。

为什么要分这些盒?

这是因为 CSS 的排版逻辑是分层进行的:

层级作用举例对应上下文
Block Box控制垂直排列段落、div、sectionBFC
Line Box控制每行内容每一行文字IFC
Inline Box控制行中元素span、aIFC
Glyph Box控制字形形态“A”、“你”字体引擎层
Image Box控制图片<img>IFC

这就像打印机排字一样:

页面是由块组成的,
块中有行,
行中有行内内容,
行内内容由字与图组成。


关键认识:盒子的“包裹关系”
  • 块盒(block box) 包裹一组 行盒(line box)
  • 行盒(line box) 包裹若干 行内盒(inline box)
  • 行内盒(inline box) 进一步包裹 字形盒 / 图片盒

即:

块流包裹行流,行流包裹字形流。


层级嵌套

格式化上下文**(Formatting Context)**

CSS 的排版机制规定了:

每个盒子都有“自身的格式化上下文”(注意这个上下文不一定是由自己创造的,就像一个人属于某一阵营,但这个阵营不见得是自己建立的,也可能是加入了别人的阵营),并遵循就近支配原则


盒子 ≠ 格式化上下文
概念本质作用
盒子(Box)元素生成的视觉容器(有宽高、边距等)创建或引用顶层盒子的上下文、放内容、显示样式
格式化上下文(Formatting Context)根盒子的容纳空间、一种规则、边界约束决定盒子与盒子属性流动与浮动的边界

一句话理解:

盒子是“被排版的对象”,
上下文是“排版的环境”。


默认共享格式化上下文延续共享

我们用最简单的例子来说明:

<div class="parent">
  <p>文字1</p>
  <p>文字2</p>
</div>

浏览器解析时:

1️⃣ <body> 创建根块级格式化上下文(root BFC)
2️⃣ .parent 是块盒(display: block),它被放进根 BFC 中。
3️⃣ .parent自己并不创建新 BFC(除非触发条件,如 overflow:hidden / float 等)。
4️⃣ .parent 的两个 <p> 也被放进同一个(根)BFC。

📊 结构图:

BFC(root)
 ├── parent
 │    ├── p
 │    └── p

所以虽然“每个盒子都存在于某个格式化上下文中”,
但它们共享同一个上下文实例
也就是说——

不是每个盒子都会创建一个新的上下文。
但每个盒子都属于某个上下文

这就是关键。

举个类比:

“每个学生都属于某个班级,但不是每个学生都要创建自己一个人的班级。”


就近支配原则

“就近支配”说的是:

盒子总是在“离自己最近的格式化上下文”中参与排版。

也就是说:

  • 如果自己创建了新的上下文,就在自己的里面排;
  • 如果自己没创建,就上交给父辈(最近的那个)去排。

比如:

<div class="parent">
  <div class="child"></div>
</div>

如果 .parent 没有触发新 BFC → .child 的排版就“上交”给上层(<html>的 BFC)。

如果 .parent 触发了新 BFC → .child 就在 .parent 的上下文内排,不再上交。


共享唯一格式化上下文导致的历史问题

在CSS1与CSS2时期,盒子之间并不是一个个独立的容器,他们共处一个网页“纸面”上(同一个根上下文BFC,但只有一个会共享),在这个唯一的上下文范围内,是纸面化思维,而容器化思维!盒子与盒子的属性在纸面上具有流动性与融合性。

比如根盒子(如,创建唯一BFC)内有盒子A与盒子B,盒子A与盒子B各自设置彼此相对的外间距,我们很容易想到把这两个间距相加,这是我们容器化的思维。但其实在早期CSS的设计中,并不是这样的容器化思维,而是纸张化思维——这个间距既是A的也是B的,那么这个间距就具有融合性,比如A说我要与B相距至少10px,而B说我要与A相距至少30px,那么这时,既能满足A又能满足B的做法,自然是让A和B之间相距30px,而不是相加!因此,会出现margin合并现象,A和B共享这一个间距。

比如根盒子(如,创建唯一BFC)包裹盒子A包裹盒子B,盒子B有一个向上的外间距margin,那么由于他们都在根盒子创建的唯一BFC内,而在这个BFC内,具有流动性,从而盒子B的外间距和往上流动,流动到A,再流动到根盒子,最终赋予给根盒子,成为根盒子的外间距,呈现出margin穿透现象,而不是B与A之间产生外间距。

理解了这一点后,后续在学习float就能明白为什么会出现float后盒子塌陷问题——因为盒子的浮动的流动范围不是包裹自己父盒子,而是根盒子的范围!他会浮出,脱离自己一层又一层的父盒子,跑到根盒子的边界——因为只有根盒子才创建了独立的BFC,只有根盒子才是容器思维,才有边界!

这也就是为什么后来会让每个盒子都可以创建自己的BFC,自成一界,让内部的盒子与盒子属性,不会流出浮出自己的范围,从而从纸面话思维,正式过渡到了容器化思维。

这里我初步引入这部分的概念,后面在讲到float时,会进一步系统、专业地讲述这一历史遗留问题与后续的发展状况。


总结对比表
盒子是否创建新上下文所属上下文备注
<body>自己(根 BFC)根上下文
<div> 默认继承上层 BFC共享环境
<div> + overflow:hidden自己(新 BFC)独立布局空间
<span>父级块的行内格式化上下文IFC 内排版

关键理解:

所谓“每个盒子都有自己的格式化上下文”,并不是指它一定创建一个,而是指它必然处于某个上下文之内

因此:

  • ✅ 语义一致:每个盒子都在上下文中;
  • ✅ 逻辑一致:但默认情况下,它们共享上层上下文;
  • ✅ 没有矛盾,只是“创建者”和“归属者”不同。

总结
关键点实际含义
“每个盒子都有自身的格式化上下文”每个盒子都被某个上下文支配(存在归属)
“默认会共享上下文”只有触发条件的盒子才会新建上下文,其余都归入上层
“就近支配原则”排版由最近的上下文控制,不会跨层传导

从代码实现的角度理解格式化上下文

BFC / IFC / FFC 等格式化上下文,本质上是浏览器排版引擎内部创建的**“对象实例”**——一种可编程的数据结构。

也就是说,它们不是抽象理论,而是真正存在于浏览器的渲染树(Render Tree)中,有对应的数据对象、边界区域、布局算法函数


从 HTML → DOM → Render Tree 的生成过程

让我们先回顾渲染的核心流程:

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());
  • 它内部再管理自己的子对象(这些子对象在此上下文内排版)。

伪代码结构如下:

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;
    }
  }
}

当浏览器解析:

<div class="parent">
  <p>文字1</p>
  <p>文字2</p>
</div>

它大致会生成这样的对象层级:

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 时会怎样?

当我们写:

.parent {
  overflow: hidden; /* 触发 BFC */
}

浏览器在构建渲染树时就会检测到这个条件,
于是:

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 ];

📊 图形化理解:

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 才提出了双层定义:

display: <outer> <inner>;

例如:

写法外层行为(在父中)内层行为(对子代)
display: block flow作为块盒出现创建一个 块级格式化上下文(BFC)
display: inline flow作为行内盒出现创建一个 行内格式化上下文(IFC)
display: block flex作为块盒出现创建一个 Flex 格式化上下文(FFC)
display: inline grid作为行内盒出现创建一个 Grid 格式化上下文(GFC)

几乎所有元素的默认 display 都同时定义了“在父中怎么表现”和“对儿子如何布局”。

后面我们会深入去讲一讲CSS 的“双层 display 机制”,这里先有一个概念。


三种基本嵌套
  • 块元素中只能直接形成块流行内流(什么时候形成块流、什么时候形成行内流,与前面讲行内流时提到的

    内部流差异的问题一样,先耐下心继续往下,逐步深入);
  • 行内元素的外层如果是块 → 它就在行内流中排列;
  • 块中再嵌套块,会产生“新块流”;
  • 行内中嵌块,会触发“断流”或“匿名块盒”机制(流在大流里流,但不能反向穿透);

块级元素嵌套块级元素 (允许)

<div>
  <p>一段文字</p>
  <section>另一块内容</section>
</div>

逻辑:
外层 <div> 形成一个块流;
内部 <p><section> 作为“块级盒”,在垂直方向依次排列。

结果直观:

[div]
 ├── [p]
 └── [section]

这一层层结构就像:

  • 一个主水渠里流着多个小浮板;
  • 每个浮板之间有间隙(margin);
  • 不冲突、不破坏主河道的流动。

块中嵌套行内元素 (允许)

<p>你好,<span>世界</span>!</p>

逻辑:

  • <p> 创建块流;
  • 块流内部自动形成行内格式化上下文(IFC)
  • 所有文字与 <span> 都在这个 IFC 中水平排列;
  • 一行放不下,就自动换行。

可视化理解:

Block Flow(垂直)
└─ Line Box(水平)
   ├─ Inline Box(文字“你好,”)
   ├─ Inline Box(<span>世界</span>)
   └─ Inline Box(文字“!”)

就像一个大浮板上,放着一排排小积木(行内元素),顺序排开。


行内中嵌套块 (不允许,断流)

<p>这是一个段落,<div>我在中间放了个 div</div>看看会怎样?</p>

问题出现了:

  • <p> 本身是块级元素,内部是行内流;
  • <div> 是块级盒;
  • 行内流里不允许出现块级盒,就像一条细水流里突然塞进一块大石头,会“堵住水流”!

浏览器怎么办?
它必须“改造流向”——于是会触发一种“断流机制”,在渲染时:

  • <p> 是行内流;
  • <div> 是块流;
  • 行内流中不能直接嵌块 → 浏览器会自动分裂 **<p>**,将其重构 为三个匿名块:
<p>这是一个段落,</p>
<div>我在中间放了个 div</div>
<p>看看会怎样?</p>

这就是所谓的 匿名块盒(anonymous block box)机制。

这体现了“文档流的层级自治”:每层只能处理自己那一类排版。

通俗理解:

浏览器发现“细流中塞了大石头”,就把细流一分为三段:
石头前一段小溪、石头本身、石头后一段小溪,
这样整个水流依然保持“自上而下”的结构。


嵌套规则总结

在文档流中:

每个盒子必须在自己所属的“流体层”中生效,不能越层。

规则描述举例
块级盒只能存在于块级流中块中可以嵌块<div><p></p></div>
行内盒只能存在于行内流中块中可以嵌行内<p><span></span></p>
行内流中不能直接出现块盒若出现则断流<p><div></div></p>
每个盒子形成自己的上下文(重点)格式化上下文彼此独立BFCIFC

再看一层:为什么这么设计?

CSS 最初就是为“文本文档”设计的,而文本天然有“层次”:

  • 段落是大的逻辑块;
  • 段落内部才有文字、图片;
  • 文字中再嵌块,就会破坏“段落结构”。

所以浏览器必须维持“从外到内逐层独立”的格式化原则:

块控制空间分区,行内控制内容排列


盒子的两个身份与格式化上下文
两个身份的表现

块级元素与行内元素,各自会形成各自的格式化上下文,去容纳其子元素,但同时,他们本身作为元素,也会成为其父元素的子元素,那么这两种不同身份,各自应当如何表现呢?

事实上,CSS 中,“元素”有两个“身份”:

  1. 它在父级流中如何表现(自己是块盒还是行盒);
  2. 它在自己内部创建什么样的流(子内容如何排版)。
元素在父级中的角色自己内部的流原理
<div>块级盒(在父中垂直排列)块级格式化上下文(BFC)用于容纳更多块
<p>块级盒(在父中垂直排列)行内格式化上下文(IFC)用于排版文字
<ul>块级盒BFC(包含 <li>每个 li 自成块
<li>块级盒IFC(文字排列)或 BFC(嵌套列表)看内容类型
<span>行内盒无(行内自身不再生成独立上下文)它只是 inline box

很多人误以为“块级元素就一定在其内部产生块流”,这是个误区。
正确的理解是:块级元素的“块性”,只定义了它在父亲那一层的行为,而不是它内部的世界。


具体示例来分清“创建”与“共享”

例 1:div 中嵌套 div

<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

<p>Hello <span>world</span></p>
元素外层行为内层规则结果
p块级盒,在根 BFC 中垂直排列内部是 IFC(行内流)内容横向排列、文字换行
span行内盒,在 p 的 IFC 中排列不创建新流参与 p 的行盒排版

解释:
p 在外层是块(block),但它的内部不是块流,而是行内流(inline flow),
这就是“块中包行”的层次结构。


例 3:触发独立上下文

.outer {
  overflow: hidden; /* 触发独立 BFC */
}

此时:

  • .outer 不再与根 BFC 共享;
  • 它创建一个真正隔离的“排版空间”;
  • 子元素 float、margin 不会影响外部。

这才是“独立的格式化上下文(new context)”。


总结核心逻辑图
每个元素:
 ├─ 外层身份:作为子参与父的上下文(共享或隔离)
 └─ 内层身份:作为父支配自己的子(定义排版规则)

📘 统一认知:

  • “创建格式化上下文” = 定义内部布局规则;
  • “共享格式化上下文” = 没有触发新的独立空间;
  • 它们并不矛盾,是不同层级的逻辑。

<div><p>内部流差异的问题:第二身份摇摆不定——我是底下的盒子是跟我一起在BFC的这大家庭,还是我成立IFC去管他们?
谁决定“内部流类型”?

这是浏览器渲染排版的一个底层算法:

每个盒子在渲染时会经历:

  1. 确定自己的外部表现形式(display 的外层部分);
  2. 根据内部内容类型创建对应的“格式化上下文”(Formatting Context)。

这个过程由**HTML层的内容语义模型(Content Model)CSS层的布局模型(Display Model)**共同决定。


display 的双层模型

这里我们再次来看display 的双层模型CSS3 才正式明确语法,但其理念自CSS1就确定并应用)这个概念,方便我们理解CSS布局模型:

其实 display 包含两层含义:

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布局模型
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)嵌进去后,怎么排版

举例:

<p>这是段落 <span>行内</span>。</p>   ✅ 合法
<p>这是段落 <div>块级</div>。</p>     ❌ 非法
  • CSS 的角度pdisplay: 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 displayblock flowblock 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:

<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: hiddendisplay: inline-block 等属性。

触发条件非常简单:

当一个盒子包含行内级内容(文字、<span><a> 等),则会自动创建一个“行内格式化上下文(IFC)”来排列这些内容。

也就是说:

<p>Hello <span>world</span></p>

中,<p> 内部自动进入了 IFC 模式。

这时:

  • “Hello ” 是一个匿名行内盒;
  • <span> 是一个行内盒;
  • 它们一起被 IFC 管理,形成“行盒(line box)”;
  • <p> 的高度由这些行盒决定。

总结:BFC 与 IFC 的嵌套关系(类比图)

可以这么想:

┌────────────────────┐
│ div(BFC 容器)    │ ← 管理块级盒子排列
│ ┌────────────────┐ │
│ │ p(块盒)       │ │ ← 参与 div 的 BFC
│ │ ┌────────────┐ │ │
│ │ │ Hello <span> │ │ │ ← p 内自动形成 IFC(行内格式化上下文)
│ │ └────────────┘ │ │
│ └────────────────┘ │
└────────────────────┘

所以:

  • <div> 创建了一个 BFC
  • <p> 是这个 BFC 的子块;
  • <p> 的内部自动形成 IFC,独立负责内部的文字与行内布局;
  • 这两个上下文独立负责不同维度的排列,不相互干扰。

为什么这种嵌套很重要?

它解释了许多 CSS 现象,例如:

  1. 文本排版不受外层 margin 折叠影响
    因为文字处于 IFC 内部,而 margin 折叠只在 BFC 层级之间发生。
  2. **vertical-align**** 只能在 IFC 内起作用**
    因为它是行内盒子的相对对齐,而 BFC 不理解 vertical-align。
  3. 行高(line-height)计算在 IFC 中进行
    因为每个行盒的高度由 IFC 决定,而不是 BFC。

具体我们会在下面逐个深入。


简要总结
概念全称管理对象触发条件是否独立上下文
BFCBlock Formatting Context块级盒子之间的布局float
overflow
display: flow-root
IFCInline 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. 问题场景

看这样一段代码:

<div>
  <p>Hello</p>
  <p>World</p>
</div>

默认情况下:

  • <div> 创建了一个 BFC;
  • <p> 是块级盒,参与这个 BFC;
  • 每个 <p> 自身内部又是一个 IFC,用来排字。

2. Margin 折叠发生在哪里?

margin 折叠(Margin Collapsing) 的特征是:

只发生在“块级盒子”之间的 垂直方向

比如:

  • 相邻两个块(pp)之间的 margin-bottommargin-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. 问题场景

<p>
  文本 <img src="..." alt="" style="vertical-align: middle;">
</p>

我们用 vertical-align: middle 想让图片与文字垂直居中。
但如果你写在一个块上,比如:

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),即:

display: block;   /* 独占一行,垂直排列 */
display: inline;  /* 与文字一起排,水平排列 */
display: none;    /* 不显示 */

这时的“内层内容”根本没被考虑到,因为:

  • 那时的网页内容主要是文字;
  • <p> 里的文字都是纯文本,不存在“复杂子结构”;
  • 没有表格、没有浮动、没有定位。

所以早期的浏览器默认认为:

“块元素内部就是一堆行(inline context)。”

因此:

<p>文字文字文字</p>

👉 <p> 自己是“块流(block)”,
但它的内部内容是“行内流(inline)”。

这就是为什么:

p 是块级元素,却只能排行内内容。


CSS2 时代(1998):引入“双层 display 模型”

网页变复杂了,人们开始需要控制表格、浮动、列表、弹性排布。
W3C 于是重新定义了 display

display = <outer-display> + <inner-display>

即:

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一个人“在家里的生活方式”

举例:

display: block flex;

表示:

  • 这个盒子在外面表现为一个“块”(独占一行);
  • 但它内部用“弹性布局”排列子项。

为什么 CSS 只写一个值?

因为:

  1. 历史兼容性——早期浏览器(CSS1、CSS2)根本不识别双值;
  2. 用户友好性——写一个词就够了,浏览器帮你补;
  3. 简洁直观——大多数开发者并不关心内外层的细节;
  4. 可扩展性——CSS3 之后,标准仍保留了双层结构,只是“语法层面上简化”。

在现代 CSS 中,双层写法重新被正式支持

CSS Display Level 3(2020s)终于允许我们显式写双值

display: block flow;
display: block flex;
display: inline flow-root;
display: block grid;

这样写是完全合法的。

而且还引入了新的内层类型:

  • flow-root:开启新的块格式化上下文(相当于 overflow:hidden hack)
  • contents:让元素本身消失,只保留子元素结构
  • table / ruby:用于表格或注音排版

演化对照表

时代写法语义层数浏览器处理逻辑举例
CSS1display:block;单层(outer)默认内层为 flow简单文字排版
CSS2display:block;双层(outer+inner)隐式推断 inner块流中嵌行流
CSS3display:flex;双层(outer+inner)outer=block, inner=flex弹性盒布局
CSS4+display:block flex;双层(显式)明确两层含义可定制排版模型

补充:display: inline-block 的双层结构与含义

先看它的标准定义:

display: inline-block;

这是一个复合 display 类型,展开后其实是:

display: inline flow-root;

outer display:inline

表示这个盒子在外部的行为方式。

  • 像文字一样 在行内排布;
  • 会与文字或其他 inline 元素在同一行中并排出现
  • 受到基线(baseline)对齐的影响。

换句话说,它不是独占一行,而是一个“能进文字队列”的盒子。


inner display:flow-root

表示它内部的内容如何排布。

  • 它内部是一个独立的块级格式化上下文(BFC)
  • 可以在内部放任何块元素;
  • 内部不会与外部流相互影响(不会被浮动塌陷或 margin 合并干扰)。

因此:

🔸 它“在外面看起来像个字”,
🔸 但“里面却是一个小的独立世界,可以正常排版块内容”。


小结
层级含义
outer displayinline像文字一样并排
inner displayflow-root内部自成一格,像小型 div

所以 inline-block 就像一个微型独立排版岛

“能插入句子里的一整个小盒子。”

这实际上就解决了我们前面提到的行内流中不能插入块流的问题!


举例说明:

文字 <div style="display:inline-block;">A<br>B<br>C</div> 文字

视觉效果:

      A
      B
文字	C  文字

说明:

  • 这个 div 在外部被当作文字(inline);
  • 但内部有多行块内容(flow-root),自己排自己的,不影响外层。

继文档流后的发展

脱离文档流:浮动与定位的突破

文档流虽然稳定,但有时太“死板”。
于是后来人类为它开辟了逃脱机制:

技术意义结果
float元素“漂浮”在流之上文本环绕效果
position: absolute元素脱离父流,独立定位精准放置
position: fixed相对视口固定悬浮导航等
flex / grid建立新的流(新格式化上下文)容器级布局

它们都在“改变文档流的力量分配方式”。


文档流的物理隐喻

可以这样理解整个排版宇宙:

隐喻意义
“文档流” 是引力场决定元素自然落地的位置
“浮动 / 定位” 是外力改变元素的自然轨迹
“格式化上下文” 是局部时空控制该区域内的物理规则
“Flex / Grid” 是新文明重新定义流的维度与秩序

从“流”理解一切布局

1️⃣ 当你看一个布局,不要问“元素为什么在那里”,
而要问“它是在哪个流中?”

2️⃣ 判断规则:

  • 它是否在文档流中?
  • 它创建了新的格式化上下文吗?
  • 它如何与父流交互?

3️⃣ 真正掌握 CSS 的关键是:
学会“观察流动”而不是“记忆属性”。


核心总结

概念定义控制方向是否脱离流
文档流(Normal Flow)默认的排版机制上下 + 左右
BFC(块格式化上下文)块级排列环境垂直流
IFC(行内格式化上下文)行内排列环境水平流