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声明样式,它会被放入你指定的命名层中;如果没指定名字,就会放入匿名层中。
层级之间有一些规则:
-
后声明的层优先级高于先声明的层。在
@layer声明中,写在右边的层优先级高于左边的层(例如@layer base, theme;中theme优先级高于base)。 -
未显式声明在任何
@layer中的样式,会被视为处于一个 “最后声明的匿名层” (直接写在文件里的、不是被@import导入的,也叫顶级层)。在同一个 CSS 文件中,匿名层样式高于命名层。 -
通过
@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 | 作者 | 普通 |
| 4 | CSS 关键帧动画(@keyframes) | |
| 5 | 作者 | !important |
| 6 | 用户 | !important |
| 7 | 用户代理 | !important |
| 8 | CSS变换(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 CSS 和 Author2 CSS 获胜,1em 和 10px 被排除。恭喜0、3px、5px晋级👏
再看特异性。
这一步先比较层优先级,再比较选择器权重。
Author1 CSS的0属于顶级层。Author2 CSS的3px属于 顶级层。Author2 CSS的5px属于命名层namedLayer。
由于顶级匿名层优先级高于命名层,5px 被淘汰。剩下 0 和 3px。它们的选择器权重相同(都是元素选择器 li),所以0、3px都晋级!👏
再看作用域接近度。
它们都没有@scope,比不出来。恭喜0、3px进入决赛圈!🔥
最后看出现顺序。
因为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 | 作者 | 最先声明的层 最后声明的层 匿名层 行内样式 | 普通 |
| 4 | CSS关键帧动画 (@keyframes) | ||
| 5 | 作者 | 匿名层 最后声明的层 最先声明的层 行内样式 | !important |
| 6 | 用户 | 匿名层 最后声明的层 最先声明的层 | !important |
| 7 | 用户代理 | 匿名层 最后声明的层 最先声明的层 | !important |
| 8 | CSS过渡(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 表现属性 的覆盖机制,尽管它们不直接参与层叠,但也有自己的优先级处理方式。
本篇文章到此就结束,感谢观看!🌸
尽管提出批评和建议,笔者会在第一时间进行修正👍