CSS 布局原理之 Formatting Context

Recall

CSS 布局原理之盒子模型

  • 讲述了 DOM + CSSOM -> 生成 Box Tree 的细节;
  • 提到了一个 element 可以生成一个 principal box 和 0 个或者多个其他 box,而生成的其他 box 基本上都属于 principal box 的后代。

注意:下文提到的 盒子 与 box 在语义上是等价的。

Visual Formatting Model(Overview)

visual formatting model 是 CSS 规范的一个模块,他实际上定义了 UA(User Agent) 如何处理 document tree,然后该如何处理并且渲染到可视化媒体上(如浏览器 canvas)上。

先来看一个盒子的布局的影响因素:

  1. 盒子尺寸和类型(各种 display 类型)
  1. 盒子的定位方法(三种 position schema:normal flowfloatabsolute positioning
  1. 在 document tree 上的嵌套层级
  1. 其他一些信息,如 viewport 的大小,一些可替换元素的固有尺寸等(intrinsic dimensions)

而这个 visual formatting model 定义的就是上面橙色文字标注的部分。

Formatting Context

formatting context(格式化上下文)本质上是一个布局环境(layout environment),而布局环境承载着当前布局的规则。一个简单的例子就是,flex 布局和 grid 布局是两个不同的布局规则,那么他们就需要两个不同的布局环境,也就是说,flex 布局 和 grid 布局需要生成两个不同的 formatting context。

一般情况下,一种特定的盒子(如 block container、flex container)会生成一个 formatting context(但不局限于 1 个),formatting context 的种类有:

  • Block Formatting Context (BFC)
  • Inline Formatting Context (IFC)
  • Flex Formatting Context (FFC)
  • Grid Formatting Context (GFC)
  • Table Formatting Context (TFC)
  • Ruby Formatting Contect (RFC)
  • ... etc(未来可能更多)

Independent Formatting Context

一些 CSS 属性可以强制一个盒子给他们的后代元素生成一个新的格式化上下文,而不是继续使用他们当前所在的格式化上下文。

比如:

  • 如果让一个盒子脱离文档流,那么会造成一个盒子区块化(blockify),会使得这个盒子生成一个新的 independent formatting context
  • 如果给一个元素设置 contain 属性,会造成这个元素生成一个新的 independent formatting context
  • 但是在 subgrid 场景下,会造成它的内容参与父元素的 grid 上下文

可替换元素 & 不可替换元素

首先先介绍 HTML 里面一个比较特殊的元素:可替换元素

如果一个元素的渲染逻辑并非由当前的 CSS 来控制,而是由其他渲染模块来控制,同时他们的渲染逻辑并不受 CSS formatting model 的限制,一般称这种元素为可替换元素;反之为不可替换元素。

名词中的「可替换」可以简单地理解为:元素可以被外部内容替换。

一个简单的例子是:<img><video> 这两个元素,他们的渲染逻辑并不是 CSS 渲染模块来进行渲染,而是由浏览器自身的图片、视频模块来渲染,所以他们属于可替换元素。

可以简单地理解成 <img><video> 元素最后会被他们 src 所指向的内容所替换。

固有尺寸

固有尺寸(Intrinsic size) 是指一个元素的本身的尺寸,其包含三个维度(Intrinsic dimensions):Intrinsic width, Intrinsic height 和 Intrinsic aspect ratio。

一般来说,可替换元素的固有尺寸是指其引用到的对象的原始大小,如图片有其本身的大小、svg 有其本身的比例

并不是所有替换元素都有 Intrinsic dimensions

在实际渲染过程中,CSS 还会根据 Intrinsic size、 specified size 和 default object size 一起计算出来最终的 concrete object size

其中,specified size 指的是由 CSS 中定义的 widthheight 等属性来定义

default object size 指的是元素的一个默认大小,如 <``img``> 不指定大小会分配一个默认大小

然后将这个计算出来的 concrete object size 告诉外部对象进行渲染,如果没有特殊指定,这个外部对象最终会裁剪成 concrete object size 的矩形大小

Block & Inline

介绍完替换元素之后,接下来介绍一下 block 和 inline 的概念

注意下面出现各种不同的 block 和 inline 的概念,注意区分:

本身是 block、inline 的元素 处在 block、inline 布局中的元素

生成 block、inline 布局环境的元素

element 一般指 DOM 树上面的节点

box 一般指 box tree 上面的节点

block-level

参与 block 布局的内容

block-level box:参与 block 布局的盒子

block-level element:参与 block 布局的元素,一般会生成一个 box-level principal box

inline-level

参与 inline 布局的内容

inline-level box:参与 inline 布局的盒子

inline-level element:参与 inline 布局的元素

block container box

一个 block container box 是一个生成 / 参与 IFC 或者 BFC 上下文的盒子,它一般只包含 inline-level box 的盒子(意味着参与 IFC 布局,后面有介绍),或者只包含 block-level box 的盒子(意味着参与 BFC 布局,后面有介绍)。

正常开发情况下,有 inline-level 和 block-level 混在一起的情况,这种时候会通过生成匿名盒子来完成这种区分

  • 如果是前者,那么这个盒子会生成一个新的 inline formatting context
  • 如果是后者

    • 如果它的父盒子的 formatting context 不是 BFC,那么这个盒子会生成一个新的 BFC;(这里可以解释为什么display: inline-boxflex|grid itemstable-*等可以创建一个新的 BFC,因为他们跟父元素之间的 formatting context 不同,一般需要生成一个新的 formatting context)
    • 如果它的父盒子的 formatting context 是 BFC,那么这个盒子可能会继续使用这个 BFC,又或者是创建一个新的 BFC(根据一些属性来决定)

下面是两种比较常见的布局情况:

case1:当一个 block-level element 同时包含 inline-level element 和 block-level element 的情况

生成一个匿名盒子 强制使得上面的 DIV 元素只有 block-level box

这个匿名盒子的 CSS 可继承属性从距离其最近的 non-anonymous box 继承

case2:当一个 inline-level element 包含 block-level element 的情况

这个 inline box 被 block-level box 打断,将 inline-level 内容分成了两个部分。会生成两个匿名的 block box

但是一些 CSS 属性还是应用到相应的行内内容和子盒子,如上图的 border 渲染情况

在解析一下百分数的时候,匿名 block box 会被忽略,真正使用的盒子是最近的非匿名祖先盒子

block box

一个盒子即是 block-level box,又是 block container box

inline box

其内容跟其本身一起参与同一个 IFC 的盒子

下面这张图可以复习一下上面提到的一些概念:

图片来源:yachen168.github.io/article/Blo…

containing block

一个盒子的位置和大小有时候会以另外一个盒子的矩形区域为参考标准,这时候我们称另一个盒子和该盒子的 containing block

我们一般称 A 盒子的 containing block 指的是 A 盒子所存在的 containing block(可能是 B 盒子生成的 containing block),而不是 A 盒子生成的 containing block

虽然这个盒子是以另外一个盒子作为参考的,但是并不意味着这个盒子一定是在另一个盒子的矩形区域内,它可能会 overflow,超出该矩形范围

定义一个盒子的 containing block 有一些规则:

  1. 根元素所存在的 containing block 被称为 initial containing block,一般来说它的尺寸大小跟 viewport 相同,并且存在于浏览器 canvas 的参考中心(origin)
  1. 非根元素,如果元素的 positionrelativestaticsticky 的时候,其 containing block 是由其所参与 formatting context 定义的。比如 display: block 的盒子,其 containing block 是距离他最近的 block container box 祖先的 content area。
  1. 如果元素的 positionabsolute 的时候,其 containing block 是由距离其最近的一个生成了 absolute positioning containing block 的祖先元素来生成,这个 containing block 一般满足下面的条件:

    1. 如果这个祖先元素不是 inline box,那么 containing block 区域则是 padding area
    2. 如果没有组件元素,那么其 containing block 就是 initial containing block

可以使得一个元素生成 absolute positioning containing block 的一些属性:position、transform、will-change、contain

  1. 如果元素的 positionfixed 的时候,其 containing block 是由距离其最近的一个生成了 fixed positioning containing block 的祖先元素来生成(一般情况下,fixed positioning containing block 也是 absolute positioning containing block),这个 containing block 一般满足下面的条件:

    1. 如果没有祖先元素,那么其 containing block 就是 viewport,并且不会随着滚动而改变位置
    2. 在打印时候,fixed 元素会重复出现在每一个页的固定位置

可以使得一个元素生成 fixed positioning containing block 的一些属性:ransform、will-change、contain

下面是一个例子:

Display 的两层含义

display type

display 属性一般包含两层含义:

  • inner display type:内部 display type,指非替换元素生成的 formatting context 类型(默认 flow、flow-root、table、flex、grid、ruby)
  • outer display type:外部 display type,指一个元素的 principal box 在 flow layout(正常流)的布局方式(默认block、inline、run-in)

只有在 ruby 下,outer display type 是 inline 而不是 block

上述的 flow 与 flow-root 的区别是指:flow 可以生成一个新的 independent formatting context,也可以复用父盒子的 independent formatting context;

flow-root 永远生成一个新的 independent formatting context

display 可以说是直接导致上述区分这么多盒子的原因之一,知道哪一种 display 可以生成哪一种类型盒子,可以在看文档的时候知道什么属性对什么类型的元素生效,如:

Block Formatting Context & Inline Formatting Context 介绍

这两个格式化上下文在上述介绍各种类型的盒子时候或多或少涉及到了其核心逻辑,下面做一些补充

Block formatting contexts

在一个 块级格式化上下文 中:

  • 盒子从 containing block 的顶部开始,在垂直方向中一个接着一个排布
  • 两个盒子的垂直距离是通过 margin 来决定的,同一个 BFC 中的两个相邻盒子之间,marign 会发生重叠
  • 在块级格式化上下文中,每一个盒子的左边界[inline-start-edage]都跟 containing block 的左边界接触(跟 writing mode 相关)

这一个规则即使是在有 浮动元素 存在的情况下也成立,这里后面的 浮动 中有介绍

margin 重叠现象:

发生在相邻兄弟元素之间和父子元素之间

图片来源:yachen168.github.io/article/Col…

一种可能的设计思想:

在 CSS2 刚出来的时代(1998),web 内容基本上都是以图文排版为主,并没有太多的布局要求,如下面的代码:

<p>article</p>
<h1>header 1</h1>
复制代码

一般来说,p 元素有一个 margin-bottomh1元素有一个margin-top,当他们二者重叠在一起的情况下,如果在排版的角度来看,他们之间的间距就会过大。如果每次都需要写一个样式来修改间距,那么就会变得十分繁琐。而 margin 重叠可以不用做其他操作就可以获得很好的展示效果,可以看出 margin 重叠在纯排版的文本上是有一定的合理性。

附:可以触发一个新 BFC 的属性:

developer.mozilla.org/en-US/docs/…

大致可以分成这几种分类:

  1. 根元素
  1. 参与的 formatting context 跟即将生成的 formatting context 不同(各种不同的 display)
  1. 显示指定:flow-root(见 inner display type)
  1. 页面渲染层级、渲染优化相关的:如脱离文档流的 float 和 position,contain,overflow
  1. 理论上还有一些其他级别的元素,如提升到了 composition layer 的元素等

Inline formatting contexts

一个 IFC 不能够被显示创建,它一般在下面这种情况下被创建:当一个 block container box 的子元素都是 inline-level box 的时候,这时候 block container box 就会创建一个 IFC

在一个 行内格式化上下文 中:

  • 盒子是从 containing block 的顶部开始,一个接着一个在水平方向上进行排布,他们的水平方向上的 marginpaddingborder 依旧正常生效
  • 水平方向的盒子可以通过不同的方式来对齐(如 A 的 top 位置跟 B 的 middle 位置 再跟 C 的 base-line 对齐),一行内包含这些 inline-level box 的盒子叫 line box

  • line box 的宽度是由 containing block 和 浮动元素共同来决定的, line box 的高度是根据下面的逻辑来计算的:

    • 如果是可替换元素、inline-block 等 inline-level 但不是 inline box,那么高度将是其 margin box 的区域高度
    • 如果不是,这个高度将由 line-height 来决定
    • 在垂直方向上对齐之后(一般通过 vertical-align 来指定),line box 最终的高度是最高的盒子的顶部到最低盒子的底部之间的距离
  • 当一个 line box 里面的内容宽度比 line box 的宽度还要少的时候,那么通过 text-align 来决定他们的水平方向对齐方式。也就是说,对齐影响的是内容在 line box 中的分布
  • 当一个 inline box 超出了当前 line box 的宽度,那么它会被分成多个 box 分布到不同的 line box 中,如果不能被分隔(如只有一个字的情况下),这个盒子将会溢出(overflow)

Float

最后介绍一下布局的一个“梦魇”:浮动布局

浮动布局一般用来做一些图文环绕的效果,和一些简单的布局,随着布局形式的增加现在开发中已经基本上不会用到浮动

浮动布局是指一个盒子浮动到当前行的最左边或者最右边,而其他非浮动的内容会环绕着浮动内容。如下图

内容一般在左浮动元素的右边和在右浮动元素的左边。

一个浮动盒子会左/右偏移,直到他的一个边界(margin box)碰到了 containing block 的边界或者另一个盒子的边界。如果当前行有 line box,那么浮动盒子将会与 最顶层的 line box 的顶部对齐:

同时,浮动会导致其所占据的 line box 宽度变短,如上图,因为浮动的存在,line box #2 和 #3 的宽度变窄了,而 text-align 是根据 line box 来进行作用的,所以上述如果设置了 text-align: right,那么会形成右环绕的效果。

如果 浮动元素 margin box 的宽度是 0 或者 负数,那么它将不会压缩 line box 的宽度。

见下图,如果增加一个浮动元素,浮动元素将会浮动到最左边,同时所占据的 line box 宽度被压缩,没有被影响的 line box 宽度不变

图片来源:yachen168.github.io/article/Lin…

而 text-align 作用范围是 line box,所以居中是根据剩余空间来进行居中的:

图片来源:yachen168.github.io/article/Lin…

如果浮动元素过大,不足以容纳其他元素(也包括浮动元素),那么这些其他元素将会换行直到有足够的空间来布局:

图片来源:yachen168.github.io/article/Lin…

由于浮动是脱离文档流的,所以它没有实体高度,当一个盒子里面只有浮动元素的时候(或者浮动元素比实体元素高度还要高的时候),会发生高度塌陷的问题,这时候可以通过 clear: fix 来清除浮动所造成的影响。

图片来源:yachen168.github.io/article/Lin…

另外建立一个新的 BFC 也可以清除浮动所造成的影响:

对容器盒子建立一个新的 BFC:

图片来源:yachen168.github.io/article/Lin…

对黄色盒子建立一个 BFC:

图片来源:yachen168.github.io/article/Lin…

BFC 可以清除浮动的原理:

由于浮动是通过影响 line box 来形成的效果,一般情况下,一个盒子会参与其父元素所在的 BFC,这样的话,浮动可以影响这个 BFC 范围内的 line box(所以它会高度塌陷,因为下面可能有其他生成 line box 的盒子)。

如果新建了一个 BFC,那么这个盒子内的 line box 跟 外面 BFC 的 line box 将处于不同的上下文,浮动元素不能够横跨两个上下文来影响元素,所以其造成的影响也会消失。

一般而言,在不同 formatting context 是相互隔离的,一般 formatting context 内部的元素不会影响外面布局。

Epilogue

本篇涉及到新名词较多,而且规范中其实还有特别多细节,这里就没有一一列出来了,感兴趣的同学可以去参考文章里面再详细了解。

References

↓ CSS2 主要参考

  1. developer.mozilla.org/en-US/docs/…
  1. www.w3.org/TR/CSS22/vi…

↓ CSS3 主要参考

  1. www.w3.org/TR/css-disp…

↓ 可替换元素

  1. developer.mozilla.org/en-US/docs/…
  1. juejin.cn/post/684490…
  1. developer.mozilla.org/en-US/docs/…
  1. drafts.csswg.org/css-sizing-…

↓ 布局各种名词

  1. www.w3.org/TR/css-disp…

↓ display 类型

  1. yachen168.github.io/article/dis…

↓ line box 介绍

  1. yachen168.github.io/article/Lin…

↓ 其他参考

  1. www.w3.org/TR/css-posi…

加入我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

扫码发现职位&投递简历

官网投递

job.toutiao.com/s/FyL7DRg

猜你喜欢

转载自juejin.im/post/7103839082668621838