你真的懂 CSS 吗?一文看懂“层叠”的底层机制!(含 MDN 原文解读)

352 阅读14分钟

MDN原文链接: Introducing the CSS Cascade

引言

Hi,你有没有想过,为什么 CSS 全称是“层叠样式表”(Cascading Style Sheets),“层叠” 二字究竟意味着什么?为什么要强调是 “层叠” 样式表呢?今天,笔者就带领大家阅读 MDN 文档的 Introducing the CSS Cascade 一节,深入理解 CSS 层叠机制,希望本文能对你有所帮助!

注:有误的地方,恳请批评指正!

一、什么是层叠(Cascade)

层叠 是 CSS 中的一项核心算法。浏览器允许为同一元素的同一属性设置多个候选值,但最终只会采用一个。层叠就是用来选出这个最终值的。这也是 CSS 名字要强调层叠的原因。(没选上的可以看到在开发者工具里被划掉了)

具体来说,层叠算法决定了当来自不同 来源(origin)层(cascade layer)@scope对同一个元素的某个属性设置了多个值时,哪个值具有更高优先级并最终生效。

在深入层叠规则之前,我们先来了解几个重要的术语。

二、相关术语

1、来源(origin)

来源 指的是 CSS 样式表的“出处”。CSS 样式主要有三种来源: 用户代理样式表(User-agent stylesheets)作者样式表(Author stylesheets)用户样式表(User stylesheets)

用户代理样式表(User-agent stylesheets)

用户代理,通常是浏览器,为文档提供的默认样式,如 <a> 标签的下划线、<h1> 的字体大小等。只有极少数浏览器支持用户修改用户代理样式表。大多数浏览器是通过实际的 CSS 文件实现这些默认样式的,但也有少数浏览器是直接用在底层代码里写死的,不过最终作用是一样的,就是为文档提供默认样式。

不同浏览器的默认样式可能略有差异。不过开发者们可以使用一个 CSS 文件来统一这些默认样式(比如 normalize.css )。除非浏览器默认样式带有 !important,否则开发者编写的样式优先级更高。这里涉及到 来源(origin) 的覆盖规则,待笔者稍后再说。

作者样式表(Author stylesheets)

由网页开发者编写的样式,这也是我们最常接触的样式。它们通过 <link> 标签、<style> 块或 style 属性(内联样式)来定义,决定了网站的视觉呈现。

用户样式表(User stylesheets)

用户通过浏览器配置或扩展程序自定义的样式,以满足个性化需求。直接配置的方式可以参考 user styles can be configured 这篇文章。

题外话:笔者使用插件 Stylus 修改网页样式,感觉还不错。 😋☝️

2、层(Cascade layers)

是 CSS 为了解决复杂项目中样式冲突而引入的一种分组机制。它允许你在 同一来源 内,进一步细分和管理样式的优先级。

样式可以被放置在 命名层 (如 @layer base;)或 匿名层 中。用 layer、 layer()@layer声明样式,它会被放入你指定的命名层中;如果没指定名字,就会放入匿名层中。

层级之间有一些规则:

  1. 后声明的层优先级高于先声明的层。在 @layer 声明中,写在右边的层优先级高于左边的层(例如 @layer base, theme;theme 优先级高于 base)。

  2. 未显式声明在任何 @layer 中的样式,会被视为处于一个 “最后声明的匿名层” (直接写在文件里的、不是被 @import 导入的,也叫顶级层)。在同一个 CSS 文件中,匿名层样式高于命名层。

  3. 通过 @import 导入的未命名层也属于匿名层,但它们不叫顶级层,优先级低于直接写在文件中的顶级层。

举个栗子🌰:

@import url("theme.css") layer(theme); /* 命名层:theme */
@import url("theme.css"); /* 匿名层,但优先级低于 h1 { color: red; } */

@layer base { /* 命名层:base */
  h1 { color: green; }
}

h1 { color: red; } /* 顶级匿名层,优先级最高 */

在此示例中,优先级顺序为:顶级匿名层 > @import 匿名层 > base 层 > theme 层。

如果在开头使用@layer改变层级优先级:

@layer base, theme; /* 改变层级优先级:theme 优先级高于 base */
@import url("theme.css") layer(theme);
@import url("theme.css");

@layer base {
  h1 { color: green; }
}

h1 { color: red; }

此时优先级变为:顶级匿名层 > @import 匿名层 > theme 层 > base 层。

三、层叠规则

之前提到 层叠 是决定哪个样式被应用的算法,它按以下顺序进行比较:

  • 1. 相关性(Relevance)

  • 2. 来源和重要性(Origin and Importance)

  • 3. 特异性(Specificity)

  • 4. 作用域接近度(Scoping Proximity)

  • 5. 出现顺序(Order of Appearance)

步骤依次执行。只要在某一步的比较里出局,就直接淘汰,后续比较步骤它不再参与,类似于CSS选择器权重的比较。每一步的比较都是基于前置步骤结果相同的条件下进行的。

比如说,在用户代理样式没有 !important 属性的前提下,作者样式优先级会比用户代理样式高。用户代理样式表在来源步骤已经出局,不会参与特异性之类的后续步骤。也就导致:即使用户代理样式中的选择器具有更高的特异性,最终也会采用作者样式表中的样式。


接下来,我们详细解释每一步:

1、相关性(Relevance)

这是过滤样式的第一步。不符合条件的样式,例如不满足 @media 声明条件或选择器未命中的样式,会直接在此步被排除,不再参与后续比较。

举个栗子🌰:

@media print {/* 如果当前设备不是打印机,那么这条规则会失效 */
  p { color: blue; }
}

.pig {/* 如果文档里没有 class 为 pig 的元素,那么这条规则会失效 */
  p { color: blue; }
}

2、来源和重要性(Origin and Importance)

这一步确定了不同来源和重要性标记下的样式优先级。关于这一部分,后续还有一些细节,这里我们先产生一个整体的认知。

优先级顺序(低到高)来源重要性
1用户代理普通
2用户普通
3作者普通
4CSS 关键帧动画(@keyframes)
5作者!important
6用户!important
7用户代理!important
8CSS变换(transition)

也就是说:

  • 动画优先级高于所有普通值(不管是来自用户、开发者还是用户代理)。
  • 所有重要值(不管是来自用户、开发者还是用户代理)优先于动画。
  • 变换优先级最高,高于重要值。

3、特异性(Specificity)

在来源和重要性相同的样式中,特异性(即我们常说的选择器权重)决定了哪个样式生效。层的优先级在此步中也需要考虑。层的优先级分散在文章里讲解,而对于选择器权重,这里笔者不再赘述。

4、作用域接近度(Scoping Proximity)

当两个来自相同来源和层的选择器特异性相同时,会比较它们到 @scope 根的接近度。距离作用域根更近(向上跳 DOM 层级所需步数更少)的样式将胜出。更多信息请参考:How @scope conflicts are resolved 

举个栗子🌰:

<div id="app">
  <section>
    <p>我是文字</p>
  </section>
</div>
@scope (#app) {
  p { color: red; }
}

@scope (section) {
  p { color: blue; }
}

这里 <p> 被两个 @scope 匹配:

  • #app<p> 要跳两层(div → section → p)
  • section<p> 只跳一层

所以最终胜出的是 color: blue;,因为它离作用域根更近☝️。

5、出现顺序(Order of Appearance)

这是层叠算法的最后一步。如果以上所有比较步骤都无法确定胜出者,那么后声明的样式获胜。


了解了层叠算法的大致步骤,我们可以来尝试一下。

假设我们有以下文件:

User-agent CSS

li {
  margin-left: 10px;
}

Author1 CSS

li {
  margin-left: 0;
} /* This is a reset */

Author2 CSS

@media screen {
  li {
    margin-left: 3px;
  }
}

@media print {
  li {
    margin-left: 1px;
  }
}

@layer namedLayer {
  li {
    margin-left: 5px;
  }
}

User CSS

.specific {
  margin-left: 1em;
}

HTML

<ul>
  <li class="specific">1<sup>st</sup></li>
  <li>2<sup>nd</sup></li>
</ul>

正如刚才的五个步骤,我们要——

首先看相关性。

如果当前设备不是打印机(会有人用打印机看这个吗?),所以Author2 CSS 中的 @media print 规则(margin-left: 1px;)被排除,恭喜10px, 0, 3px, 5px, 1em晋级👏

接下来再看来源和重要性。

全部都是普通的属性,那么优先级就是 作者 > 用户 > 用户代理User CSS 在来源与重要性的比较中,也就是在特异性比较开始之前就被淘汰了,所以即使 .specific 是一个类选择器,优先级高于 li 标签选择器,最后胜出的也会是 Author 里面的 li。于是 Author1 CSSAuthor2 CSS 获胜,1em10px 被排除。恭喜03px5px晋级👏

再看特异性。

这一步先比较层优先级,再比较选择器权重

  • Author1 CSS0 属于顶级层
  • Author2 CSS3px 属于 顶级层
  • Author2 CSS5px 属于命名层 namedLayer

由于顶级匿名层优先级高于命名层,5px 被淘汰。剩下 03px。它们的选择器权重相同(都是元素选择器 li),所以03px都晋级!👏

再看作用域接近度。

它们都没有@scope,比不出来。恭喜03px进入决赛圈!🔥

最后看出现顺序。

因为Author2 在 Author1 后面定义,所以 Author2 CSS 胜利了。恭喜3px成为最后的赢家,被浏览器选中参与页面绘制!🏆


好了,读到这里,大家对层叠的规则有了整体上的理解。正如我们之前提到的,作者样式表里面可以有内联样式表(inline style)。值得注意的是,只有“作者样式表”中可以出现内联样式表 ,用户代理样式表和用户样式表都没有“内联样式”这种形式。

内联样式表、层和 !important 之间也有层叠规则。接下来笔者就带着大家继续深入👉

四、行内样式对层叠的影响

有一些基本规则:

  • 普通行内样式 优先于所有普通的外部/内部作者样式,无论选择器特异性如何。

  • 动画 (@keyframes) 和过渡 (transition) 中的样式会覆盖普通行内样式。

  • 带有 !important 的行内样式会击败所有其他作者样式。

  • CSS 过渡动画 优先级最高,可以击败包括 !important 行内样式在内的所有样式。

有三种方式可以击败 行内!important

  • 用户样式表的 !important 样式。
  • 用户代理样式表的 !important 样式(这种情况极少见)。
  • CSS 过渡动画。这也是唯一在 作者样式表中可以打败 行内 !important 的机制

五、!important 对层叠的影响

!important 会“反转”一些优先级规则:

1、来源优先级反转

对于带有 !important 标记的样式,来源的优先级顺序会反转

  • 正常情况(普通样式):作者样式 > 用户样式 > 用户代理样式

  • !important 情况:用户代理 !important > 用户 !important > 作者 !important

这意味着,越“底层”的 !important 优先级越高

2、层优先级反转

在涉及 @layer 时,带有 !important 的样式优先级也会反转:

普通样式:后定义的层覆盖先定义的层。

!important 样式先定义的层优先级更高

尽管如此,层的优先级总是在行内样式之下。


关于 !important 的最佳实践

基于上述缘由,最好不要用 !important 去强制重写外部样式,而是使用 @import 和 layer 来降低它们的优先级。

通过在 CSS 文件开头使用 @import layer(...) 引入外部库,可以“降权”这些外部样式。然后,你再编写自己的 @layer components { ... } 或普通样式,它们的优先级会更高,可以轻松覆盖框架样式。

只有在需要“确保不被覆盖”的极端情况下,才可以在最先声明的层中使用 !important

举个栗子🌰:

@import url("reset.css") layer(framework); /* 将 reset.css 导入到名为 framework 的层中 */
@import url("my-styles.css"); /* 普通导入,属于匿名层,优先级高于 framework 层,低于顶级层 */

/* 或者这样使用 @layer 块 */
@layer framework {
  @import "reset.css";
}

body { /* 顶级层样式,优先级高于任何命名层 */
  color: black;
}

看到这里,层叠的部分基本就结束了。是时候献上完整的层叠规则表了——

六、完整层叠表

优先级(从低到高)来源优先级(从低到高)重要性
1用户代理最先声明的层

最后声明的层

匿名层
普通
2用户最先声明的层

最后声明的层

匿名层
普通
3作者最先声明的层

最后声明的层

匿名层

行内样式
普通
4CSS关键帧动画 (@keyframes)
5作者匿名层

最后声明的层

最先声明的层

行内样式
!important
6用户匿名层

最后声明的层

最先声明的层
!important
7用户代理匿名层

最后声明的层

最先声明的层
!important
8CSS过渡(transition)

补充:不参与层叠的规则

说了那么多,那么哪些东西会参与层叠呢?

并非所有 CSS 相关的东西都参与层叠。只有 CSS 属性/值对才参与层叠。而 @规则 中的描述符(descriptors,注意是描述符不是属性/值对@规则里面可以有描述符也可以用属性/值对,它们是不一样的)和 HTML 表现属性(presentational attributes)则不参与层叠,它们有自己的覆盖规则。别急,笔者马上介绍👉

1、@声明的覆盖规则

大多数情况下,@规则 内部定义的属性和描述符不参与层叠,而是 @规则 作为整体来参与层叠。

1.1 @font-face

比如这里有两个@font-face声明:

@font-face {
  font-family: "MyFont";
  src: url("A.woff2") format("woff2");
  font-weight: 400;
}

@font-face {
  font-family: "MyFont";
  src: url("B.woff2") format("woff2");
  font-weight: 400;
}

浏览器不会对其中的每一条属性进行比较,而是比较这些 @font-face 整体,选出最适合的那个。如果有多个同样合适的,那么浏览器就会依次根据 来源和重要性 以及 出现顺序 来决定( @规则 没有特异性,所以不用考虑)。

1.2 条件性@规则

虽然在大多数 @规则(如 @media, @document, @supports)中包含的声明,是参与 层叠 的(即参与层叠过程),但前提是这个 @规则 本身的条件成立,否则整个规则会被当作“无关(not relevant)”,直接跳过。这也就是我们层叠算法的相关性步骤。

1.3 @import

@import 规则本身不参与层叠,但它导入的样式会参与。如果 @import 定义了命名层或匿名层,导入的样式内容会被放入指定的层中。所有未指定层的 @import 导入的样式,会被视为 最后声明的层 ,但优先级低于顶级样式(即未被 @import 导入且未声明层的样式)。

举个栗子🌰:

@import url("reset.css"); /* 未指定层,导入样式视为最后声明的层 */
@import url("reset.css") layer(reset); /* 导入样式放入命名层 reset */

body { /* 顶级匿名层样式 */
  color: black;
}

1.4 @charset

@charset 规则用于指定样式表的字符编码,它在解析字节流之前就被移除了,因此不参与层叠。

1.5 @keyframes

@keyframes 定义的动画不参与层叠。如果同一个来源、同一个层中有多个同名动画,浏览器会使用 最后出现的那个 。浏览器不会混合多个动画帧的定义,只会使用完整的一套,其余的全部忽略。

2、HTML表现属性的覆盖规则

HTML 表现属性(HTML presentational attributes) 是直接写在 HTML/SVG 标签上的、能控制样式的属性。比如:

<td align="center">  ← HTML 表现属性align="center"(已废弃)
<circle fill="red"> ← SVG 表现属性fill="red"(仍常用)

这些表现属性虽然是作者写的,看上去属于 作者样式 ,但它们不参与 CSS 的层叠。如果浏览器支持这些属性,就会把它们转换成等价的 CSS 规则, 然后插入到作者样式的 最前面 ,而且转换后的规则特异性为 0,因此很容易被其他 CSS 规则覆盖。HTML表现属性不能被声明为 !important。


总结

本文介绍了 CSS 的层叠规则,这是 CSS 名字来源的核心。层叠从 相关性来源和重要性特异性作用域接近度出现顺序 来确定属性优先级。同时,我们还特别讨论了行内样式!important 标记对层叠规则的特殊影响,以及 @规则HTML 表现属性 的覆盖机制,尽管它们不直接参与层叠,但也有自己的优先级处理方式。

本篇文章到此就结束,感谢观看!🌸

尽管提出批评和建议,笔者会在第一时间进行修正👍