Document Flow and CSS Formatting Model

September 11, 2018 by Tian Zhi

理解基本的文档流我们就能知道HTML文档显示在Web的过程,当我们需要复杂的布局时,就不得不认真思考诸如'BFC', 'Stacking Context'等名词,了解清楚了,才能在处理布局上更加游刃有余

前言

最开始学习CSS的时候,我一度认为CSS基础很简单,但是CSS布局是一块难啃的骨头。又到后来,我去亚马逊买了畅销书HTML and CSS, design and build websites,书上内容浅显易懂,但是关于CSS布局,也就只有一章,而且似乎也没有提到复杂的文档流,我印象中也并不知道'BFC', 'Stacking Contexts'这些名词

最近的复习中不断出现'负margin','BFC','Stacking Contexts'等等概念,又在MDN上了解到CSS Visual Formatting Model,我觉得有必要系统地梳理一遍思路

本文以MDN Visual Formatting Model 这篇文章作为引导,并加入自己的理解和新增的知识点,不会涉及太多的布局真实场景,例如双飞翼布局,圣杯布局,多列等高布局等等,这里只作为基础

CSS标准盒子模型(Box Model)

CSS标准盒子模型是呈现每一个独立元素的基础,组合起来构成文档流

以下是一个标准的盒子,由4个部分组成

boxmodel

标准盒模型我们设置width属性的时候,width = content-width,怪异盒模型width = content-width + padding-width + border-width

当我们设置背景色或者背景图片时,默认会延伸到border外围,但是Z-Ordering的顺序在border以下,也就是说,如果同时设置了backgroundborder属性,border区域还是显示border,但是其Z-Order以下是background。我们可以使用background-clip属性改变这一默认行为,具体不再叙述

block元素和inline元素形成盒子的区别

<p>test</p>
<p>test</p>
<a href="">test</a>
<a href="">test</a><br />
<a href="">test</a>
<a href="">test</a>
p, a {
  width: 100px;
  height: 40px;
  border: 1px solid red;
  margin: 20px;
  padding: 30px;
}

inline vs block box model1 图一

inline vs block box model2 图二

仔细看上面两幅图,块级和行级元素盒子有以下几点不同

  1. 块级元素盒子会自动占满一行,行级元素盒子不会
  2. 块级元素可以设置宽高,行级元素宽高设置无效
  3. 块级元素之间上下margin有折叠,行级元素设置上下margin无效,但是左右margin有效,而且不会折叠
  4. 行级元素content部分出现在正常文档流中,设置上下padding会与其他元素发生重合,左右padding不会

CSS容器盒子(Containing Block)

仅仅了解完每个元素自己形成的盒子模型是不够的,因为自身的盒子在某些时候还会受到容器盒子(Containing Block)的影响,例如

  1. 基于百分比的值来设定元素盒子的width,height,margin,padding,这里需要注意,除了height是基于父元素盒子的height之外,其余三个属性都是基于父元素盒子的width
  2. 当设置为Absolute Positioning之后,基于百分比的值来设置盒子的偏移量,例如top,left等等,此时是基于父元素盒子border内侧进行定位

关于容器盒子,我们最常认为它就是父元素的content部分,其实不然,有如下三种情况

  1. 如果子元素position属性为默认值static或者是relative,则其Containing Block为父元素的content部分
  2. 如果子元素position属性为absolute,则其Containing Block为第一个position不为static的父元素的content+padding部分,如果都是static,则为Initial Containing Block

    Initial Containing Block

    The initial containing block has the dimensions of the viewport, and is also the block that contains the htmlelement. Simply put, the absolutely positioned element will be contained outside of the html element, and be positioned relative to the initial viewport.

    简而言之,就是视窗(Viewport)

  3. 如果子元素position属性为fixed,则其Containing Block为视窗(Viewport)

知道标准盒子模型以及容器盒子模型后,需要用一定的规则将这些盒子“组装”起来,以下我们来看看“组装”方式

正常流(Normal Flow)

一旦盒子模型创建成功,接下来便是将这些盒子按照一定规则组装起来,默认的组装规则就是Normal Flow

使用position: relative或者默认的position: static,且没有设定浮动,此时元素就是在Normal Flow中

Normal Flow中,块级元素盒子垂直方向一个挨着一个,行级元素盒子水平方向一个挨着一个,一行不够之后换到下一行

如果使用position: relative,可以将盒子基于原来的位置,通过设定top,bottom,left,right进行位移

浮动流(Floats)

设置一个元素为浮动之后,这个元素盒子在一行中移动到最左边或者最右边,并且脱离Normal Flow

浮动元素对其后面的元素或者是其父元素都会产生影响,除非其后元素使用clear属性清除这一影响,我之前写过一篇从圣杯和双飞翼看浮动流的文章,里面详细解释了浮动流的过程

绝对定位(Absolute Positioning)

在Floats中,虽然盒子脱离了文档流,但是其对于之后的元素盒子或者是父元素盒子都会产生影响,除非影响被清除。但是在Absolute Positioning Scheme中,盒子可以说完全抽离Normal Flow,且不再对其他元素产生影响

此时元素盒子的位置完全基于其容器盒子(Containing Block),主要两种情况

  1. 如果绝对定位使用position: absolute,容器盒子为第一个position不为static的父元素的content+padding部分
  2. 如果绝对定位使用position: fixed,容器盒子为视窗(Viewport)

堆叠顺序(Z-Ordering)

前面提到的都是元素应该出现在文档中的位置,但是如果发生堆叠,那么显示顺序(Z-Ordering, or Stacking Ordering)是怎么样的呢?

不使用z-index

HTML文档默认显示给我们的是第0层,这也是z-index的默认值,为0

  • bottom layer (farthest from the observer)
  • ...
  • Layer -3
  • Layer -2
  • Layer -1
  • Layer 0 (default rendering layer)
  • Layer 1
  • Layer 2
  • Layer 3
  • ...
  • top layer (closest to the observer)

正常的堆叠顺序,遵循如下几个规则(顺序由底向上,如果发生重叠,只有最上的元素能被用户看到)

  1. 根元素(<html>)的backgroundborder,在堆底
  2. 没有被定位的元素(position: static),根据在HTML文档出现顺序,后面出现的在堆顶
  3. 被定位的元素(除了static),根据在HTML文档出现顺序,后面出现的在堆顶
  4. 如果使用display: flex,且对flex container下的子元素使用order属性改变顺序,也会影响到堆叠顺序,order最高的元素出现在堆顶

依据上面的规则,我们看一个例子

z ordering 1 5个DIV在文档的出现顺序为1,2,3,4,5

使用z-index

首先来看看z-index的定义

The z-index CSS property specifies the z-order of a positioned element and its descendants or flex items (children of an element with display: flex). When elements overlap, z-order determines which one covers the other. An element with a larger z-index generally covers an element with a lower one.

也就是说,z-index只能在flex-item或者positioned-items下设置才有效,否则无效

再看一个例子(在上一个例子基础上使用z-index

z ordering 2 5个DIV在文档的出现顺序为1,2,3,4,5

Stacking Context

其实我们使用z-index,实际上是形成了一个Stacking Context,不同的Stacking Context下的元素层叠顺序互不影响,它们之间的层叠顺序由上层元素之间的层叠顺序决定

创建Stacking Context有如下几种常见方法

  1. HTML文档根元素<html>创建了默认的Stacking Context
  2. 设置了position: fixed或者absolute的元素,且z-index值不为默认值auto
  3. 设置了display: flexcontainer下的子元素,且z-index值不为默认值auto
  4. 设置opacity值小于1的元素

使用z-index改变堆叠顺序

前面提到过,不同的Stacking Context下的元素层叠顺序互不影响,它们之间的层叠顺序由上层元素之间的层叠顺序决定

z-index的initial(默认值)为auto

auto

The box does not establish a new local stacking context. The stack level of the generated box in the current stacking context is the same as its parent's box.

也就是说,如果一个元素没有创建Stacking Context,则其顺序由父级Stacking Context决定,最顶层的Stacking Context为根元素<html>创建,其z-index值为0

还是先看一个例子,下面分别是文档结构和结果图

Root

  • DIV #1
  • DIV #2
  • DIV #3

    • DIV #4
    • DIV #5
    • DIV #6

z ordering 3

总结

到这里Document Flow和CSS Formatting Model也就结束了,涵盖了基本的布局规则。只有深入理解这些基础,才能更好地去理解以前的一些布局Trick,比如双飞翼布局,圣杯布局等等,这些布局建立在这些基础上,如果理解了这些内容,自己实现一些布局Trick也不是不可能,虽然现在Flex布局已经可以解决很多问题了

参考

  1. https://developer.mozilla.org/en-US/docs/Web/CSS/Visual_formatting_model
  2. https://developer.mozilla.org/en-US/docs/Web/CSS/z-index

Feel free to leave me a message @tianzhich