CSS 核心工作原理

99 阅读11分钟

1. 前言

对于网页中的每一个 HTML 元素,都有一组有限的 CSS属性 用来告诉浏览器的渲染引擎,这个元素在页面上该怎么显示。

本文将对 CSS 的基本工作原理:浏览器是如何确定页面元素每一个 CSS 属性的值是多少?从而根据这些值对元素进行渲染,做一个大致的说明。

首先,CSS 的英文全称是 Cascade Style Sheets,即层叠样式表。注意这里的 Sheets 是复数,也就是说会有多张样式表,对于元素的某个CSS属性,可能存在多个不同的值。

这样就衍生出第一个问题:如果元素某个 CSS 属性有多个不同的值,怎么从这些值里面找出那个最终用做浏览器进行渲染的值?

与之相反的第二个问题:如果样式表没有指定某个 CSS 属性的值,那么浏览器该使用哪个值进行渲染?

本文将对这两个问题进行解答。

2. 值处理过程

我们可以把 CSS 的基本工作原理看成一个函数,它的输入是一组CSS规则的集合,输出是一组确定的CSS属性值。

how-css-works.svg

CSS 的基本工作原理就是对 CSS 属性值进行处理的这个过程,这个过程叫做 Value Processing ,该过程有以下 6 个不同的阶段:

value-process.svg

2.1 声明值

声明值 (declared value) 也就是我们写在样式表里的每个 CSS 属性的值。

值处理的第一步是收集所有的声明值,元素每个 CSS 属性的声明值数量大于等于0。

2.2 层叠值

层叠值 (cascaded value) 是元素每个 CSS 属性多个不同的值在 根据层叠规则进行优先级比较后胜出的值,元素每个 CSS 属性的层叠值最多只有 1 个(也就是说 0 个或 1 个)。

2.3 规定值

规定值 (specified value) 就是指元素某个 CSS 属性如果有层叠值那就使用层叠值,如果没有层叠值那就使用 CSS 属性的默认值,元素每个 CSS 属性的规定值有且只有 1 个。

2.4 计算值

计算值 (computed value) 的意思是,如果元素某个CSS属性的值是类似相对单位 em remcal() 函数等 依赖 其他值的值,那么必须对其进行依赖计算,这个计算后产生的值就是计算值,元素每个 CSS 属性的计算值有且只有 1 个。

2.5 使用值

使用值 (used value) 和计算值基本是相同的,但是对于少数像 auto 这样的计算值,需要根据父元素的实际布局进行重新计算,将其转换成类似 100px 这样的绝对单位,元素每个 CSS 属性的使用值有且只有1个。

2.6 实际值

理论上使用值就是元素最终进行渲染时所使用的那个值,但是受到浏览器的能力限制,对于有些使用值,浏览器是无法使用的。

举个例子,有些浏览器在进行渲染的时候只能使用整数,不能使用小数。

比如我们给 font-size 赋值 14.2px ,但是浏览器无法用小数进行渲染,只能将其转化成整数 14px ,这就是实际值。

2.7 示例

上面对于这 6 种值的解释看起来可能比较抽象,因此我列出几个 CSS 规范 里提供的例子供读者进行对比理解:

属性声明值层叠值规定值计算值使用值实际值
text-alginleftleftleftleftleftleft
border-widthinheritinherit4.2px4.2px4.2px4px
width没有赋值没有赋值auto ( wdith 的默认值是 auto)auto120px120px
font-size1.2em1.2em1.2em14.1px14.1px14px
width80%80%80%80%354.2px354px

3. 层叠过程

在 CSS 值处理的 6 个不同的阶段中,作为开发者,我们能把握的只有 声明值层叠值 这两个阶段。

在从声明值到层叠值的过程中,渲染引擎会将我们声明的 CSS 属性值进行 优先级排序,产出一个最终的胜者。

cascading.svg

接下来我将讲述层叠规则的具体细节,下面这张图演示了层叠规则的进行顺序

sorting-order.svg

3.1 样式来源 (Cascading Origins)

首先,一个页面中,每一条样式都有一个来源,对于 CSS 来说,样式总共有3种来源:

  • 开发者来源 (Author Origin)

    开发者来源很好理解,就是开发者写的样式代码。

  • 浏览器来源 (User-Agent Origin)

    浏览器来源也很好理解,就是浏览器有一张默认的样式表。

  • 用户来源 (User Origin)

    用户来源听起来有点晦涩,举个列子,当我们在浏览网页时,使用网页缩放功能去放大缩小字体时,这个时候就产生了用户自定义的样式规则。

Origins.svg

3.2 来源排序

层叠规则的第 1 站就是根据 样式来源 进行优先级排序,根据带不带 !important 可以分为两层。

!important 的话,则优先级排序是:

 浏览器样式 > 用户样式 > 开发者样式

不带 !important 的话,则优先级排序是相反的:

开发者样式 > 用户样式 > 浏览器样式

Origins-Sorting-Rule.svg

在样式来源排序中,我们可以把 6 种不同的情况理解为 6 个篮筐,每一条样式看做一个单独的小球。

每个小球只能放入一个篮筐中,不存在一个小球可以同时放入多个篮筐的情况。

six-origin-buckets.svg

而实际开发中我们只需关注开发者样式这一个来源,也就是说我们只需要关注 开发者 + !importance 样式和 开发者 样式这两个篮筐。

用户样式只有在很少的情况下才会对样式产生影响,而对于浏览器样式则一般是使用 reset.cssnormalize.css 等自定义样对其进行重置。

3.3 根据 DOM 和 Shadow DOM 进行排序

Shadow DOM 是 Web Components 中的一个概念,用来封装原生 web 组件。

我们可以把 Shadow DOM 和 DOM 之间的关系理解成 Document 和 iframe 之间的关系。

本文主要讲解CSS的工作原理,因此不会对 Shadow DOM 进行过多说明,想了解这方面内容的读者可以阅读这些参考资料:

样式在 DOM 和 Shadow DOM 中的排序遵守以下规则:

dom-shadow-dom-sorting-order.svg

当遇到普通样式时,Shadow DOM 中的样式优先级比 普通 DOM 中的样式优先级高。

当遇到 !important 样式时,优先级就反过来,普通 DOM 中的样式优先级比Shadow DOM 中的样式优先级高。

3.4 行内样式

层叠规则的第 3 站是行内样式 (Element-Attached Styles)。

3.5 分层排序

层叠规则的第 4 站是 分层 (layers) 排序,这个排序规则是在新的 CSS 规范 CSS Cascading and Inheritance Level 5 中提出的。

由于分层排序是 2022 年新加入的,大部分开发者可能都不知道这个规范。

对于 Layers 具体细致的使用说明可以参考这篇博文:A complete guide to css layers (该博文的作者是Mozilla的CSS专家)。

CSS Layers 向开发者提供了一种结构化的安排 CSS 样式优先级的方式。

通过使用 CSS Layers,开发者可以将 CSS 代码进行 分层管理,不同层之间的样式优先级是不会产生互相影响的。比如可以将整个样式代码划分成以下几层:

  • 元素默认样式
  • 第三方CSS库
  • 主题样式
  • 组件样式
  • 工具样式

如果有读者对 SMACSS 和 ITCSS 这两种 CSS 方法论有了解的话,会发现 CSS Layers 的理念跟这两种方法论的理念是基本相同的。

我们可以把 层 (Layer) 看成堆叠在一起的卡片,越上层的卡片中的样式优先级越高,其中没有指定某个层的样式代码会被分在默认层,而默认层是始终放在整堆卡片最上面的。

例如下面这个图片中,优先级的顺序为:默认层 > Layer C > Layer B > Layer A

layers.svg

我们可以使用 @layer 指令来定义 层 (Layer)

/* 定义多个层,层的优先级从左到右逐渐升高,最左边的优先级最低,最右边的优先级最高 */
@layer reset, defaults, patterns, components, utilities, overrides;

/* 给导入的样式表分配一个层 */
@import url('framework.css') layer(components.framework);

/* 将样式写在某个层中 */
@layer utilities {
  [data-color='brand'] { 
    color: var(--brand, rebeccapurple);
  }
}

@layer defaults {
  a:any-link { color: maroon; }
}

/* 没有分配层的样式比分配了层的样式的优先级更高 */
a {
  color: mediumvioletred;
}

上面关于 的优先级顺序是建立在样式没有加 !important 的情况下,当 样式开始加上 !important 之后,优先级的顺序就完全相反了:

例如下面这个图片中,优先级的顺序为:

Layer A (important) > Layer B (important) > Layer C (important) > 默认层 (important)

reverse-layers.svg

同时考虑普通样式和加 !important 样式的优先级顺序为:

Layer A (important) > Layer B (important) > Layer C (important) > 默认层 (important) > 默认层 > Layer C > Layer B > Layer A

layers-sorting.svg

3.6 选择器优先级排序

层叠规则的第 5 站来到我们最熟悉的选择器优先级排序。

由于行内样式的优先级排在层叠规则的第三层,因此我这里不会像常规讲解选择器优先级的文章那样将行内样式的权重包含进去。

我们只需记住一点:行内样式的优先级比所有选择器的优先级都高

选择器有好几种不同的类型,id 选择器,类选择器,伪类选择器,属性选择器,元素选择器,伪元素选择器。

一次性记清每种选择器的优先级不是一件容易的事情,这里我讲述一个诀窍:选择范围更小的选择器优先级是更高的。

一个 class 可以对应多个元素,但是 id 只能对应一个元素。所以 id 选择器的优先级比类选择器的优先级高。

元素选择器对应所有该类型的元素,但是类只能对应该该类型中的一部分元素。所以类选择器的选择范围比元素选择器的选择范围小,因此类选择器的优先级比元素选择器的优先级高。

这样我们就记住一个基本优先级准则:id > class > element

至于其他什么伪类选择器,属性选择器和伪元素选择器,都能划分都这三类下面。

selectors-order.svg

3.7 顺序排序

当经过上面 5 步的排序后,仍然出现同一个属性有多个值的情况时,最后生效的就是顺序规则。

也就是后来居上,后面出现的值的优先级比前面的值高。

3.8 层叠规则总结

到此为止,从 声明值层叠值 这个过程中的 层叠规则 就全部讲解完毕了,最后用一张图来对整个层叠规则进行总结:

cascading-rules.svg

4. 继承和默认值

本文开头总共提出了两个问题:

  1. 如果元素某个 CSS 属性有多个不同的值,怎么从这些值里面找出那个最终用做浏览器进行渲染的值?
  2. 如果样式表没有指定某个 CSS 属性的值,那么浏览器该使用哪个值进行渲染?

前面我们讲了从 声明值层叠值 这个过程,这就是第一个问题的答案:层叠规则。

那么接着来回答第二个问题,CSS 规范给出的答案是,对于每一个 CSS 属性,都有一个默认的值 initial value

如果对于元素的某个 CSS 属性,在值处理过程中 层叠值 没有给定,那么有两种情况:

  1. 该 CSS 属性本身是一个继承属性 (inherited property),那么该属性的值从其父元素那里继承。
  2. 该 CSS 属性不是一个继承属性 (inherited property),那么该属性的值采用默认值。

5. 参考资料

CSS Cascading and Inheritance Level 5

CSS Cascading and Inheritance Level 6

How CSS works - Learn web development | MDN

Cascade and inheritance - Learn web development | MDN

CSS selectors - Learn web development | MDN

@layer - CSS: Cascading Style Sheets | MDN

A Complete Guide to CSS Cascade Layers | CSS-Tricks

Using shadow DOM - Web Components | MDN

An Introduction to Web Components | CSS-Tricks