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

MDN原文链接:Introducing the CSS Cascade

引言

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

注: 笔者对自己的理解并不能保证完全正确,于是每段后面都贴上了原文相关的部分,以供读者比较。笔者不知道要怎么把段落折起来,所以直接贴在下面会显得有点长,还请读者谅解🥲。有误的地方,恳请批评指正!

一、什么是层叠(Cascade)

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

One of the fundamental design principles of CSS is cascading of rules. It allows several style sheets to influence the presentation of a document. CSS property-value declarations define how a document is rendered. Multiple declarations may set different values for the same element and property combination, but only one value can be applied to any CSS property. The CSS cascade module defines how these conflicts are resolved.

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

The cascade is an algorithm that defines how user agents combine property values originating from different sources. The cascade defines the origin and layer that takes precedence when declarations in more than one origin, cascade layer, or @scope block set a value for a property on an element.

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

二、相关术语

1、来源(origin)

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

CSS declarations come from different origin types: User-agent stylesheets , Author stylesheets , and User stylesheets.

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

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

User-agents, or browsers, have basic stylesheets that give default styles to any document. These stylesheets are named user-agent stylesheets. Most browsers use actual stylesheets for this purpose, while others simulate them in code. The end result is the same.

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

Although some constraints on user-agent stylesheets are set by the HTML specification, browsers have a lot of latitude: that means some differences exist between browsers. To simplify the development process, Web developers may use a CSS reset stylesheet, such as normalize.css, which sets common properties values to a known state for all browsers before beginning to make alterations to suit their specific needs.
Unless the user-agent stylesheet includes an !important next to a property, making it "important", styles declared by author styles, including a reset stylesheet, take precedence over the user-agent styles, regardless of the specificity of the associated selector.

作者样式表(Author stylesheets)

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

The author, or web developer, defines the styles for the document using one or more linked or imported stylesheets, <style> blocks, and inline styles defined with the style attribute. These author styles define the look and feel of the website --- its theme.

用户样式表(User stylesheets)

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

In most browsers, the user (or reader) of the website can choose to override styles using a custom user stylesheet designed to tailor the experience to the user's wishes. Depending on the user agent, user styles can be configured directly or added via browser extensions.

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

2、层(Cascade layers)

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

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

层级之间有一些规则:

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

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

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

The cascade order is based on origin type. The cascade within each origin type is based on the declaration order of cascade layers within that type. For all origins - user-agent, author, or user - styles can be declared within or outside of named or anonymous layers. When declared using layer, layer() or @layer, styles are placed into the specified named layer, or into an anonymous layer if no name is provided. Styles declared outside of a layer are treated as being part of an anonymous last declared layer.

举个栗子🌰:

css 复制代码
@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改变层级优先级:

css 复制代码
@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 属性的前提下,作者样式优先级会比用户代理样式高。用户代理样式表在来源步骤已经出局,不会参与特异性之类的后续步骤。也就导致:即使用户代理样式中的选择器具有更高的特异性,最终也会采用作者样式表中的样式。

Unless the user-agent stylesheet includes an !important next to a property, making it "important",styles declared by author styles, including a reset stylesheet, take precedence over the user-agent styles, regardless of the specificity of the associated selector.


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

1、相关性(Relevance)

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

It first filters all the rules from the different sources to keep only the rules that apply to a given element. That means rules whose selector matches the given element and which are part of an appropriate media at-rule.

举个栗子🌰:

css 复制代码
@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)

也就是说:

  • 动画优先级高于所有普通值(不管是来自用户、开发者还是用户代理)。
  • 所有重要值(不管是来自用户、开发者还是用户代理)优先于动画。
  • 变换优先级最高,高于重要值。
  • Animations take precedence over normal values, whether declared in user, author, or user-agent styles.
  • Important values take precedence over animations, whether declared in user, author, or user-agent styles.
  • Transitions take precedence over important values.

3、特异性

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

In case of equality with an origin, the specificity of a rule is considered to choose one value or another. The specificity of the selectors are compared, and the declaration with the highest specificity wins.

4、作用域接近度

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

举个栗子🌰:

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

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

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

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

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

When two selectors in the origin layer with precedence have the same specificity, the property value within scoped rules with the smallest number of hops up the DOM hierarchy to the scope root wins. See How @scope conflicts are resolved for more details and an example.

5、出现顺序

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

In the origin with precedence, if there are competing values for a property that are in style block matching selectors of equal specificity and scoping proximity, the last declaration in the style order is applied.


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

假设我们有以下文件:

User-agent CSS

css 复制代码
li {
  margin-left: 10px;
}

Author1 CSS

css 复制代码
li {
  margin-left: 0;
} /* This is a reset */

Author2 CSS

css 复制代码
@media screen {
  li {
    margin-left: 3px;
  }
}

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

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

User CSS

css 复制代码
.specific {
  margin-left: 1em;
}

HTML

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)。值得注意的是,只有"作者样式表"中可以出现内联样式表 ,用户代理样式表和用户样式表都没有"内联样式"这种形式。

Only relevant to author styles are inline styles, declared with the style attribute.

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

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

有一些基本规则:

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

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

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

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

Normal inline styles take precedence over any other normal author styles, no matter the specificity of the selector. Normal inline styles do not take precedence over animated or transitioned properties.

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

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

Important inline styles take precedence over all other author styles, regardless of whether they are important, inline, or layered. Important inline styles also take precedence over animated properties, but not transitioned properties. Three things can override an important inline style:

  • An important user style.
  • An important user agent style.
  • A transitioned property.

五、!important 对层叠的影响

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

1、来源优先级反转

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

The origin type precedence order is inverted for important styles.

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

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

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

2、层优先级反转

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

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

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

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

Important styles declared outside of any cascade layer have lower precedence than those declared as part of a layer. Important styles that come in early layers take precedence over important styles declared in subsequent cascade layers.


关于 !important 的最佳实践

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

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

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

The !important flag reverses the precedence of cascade layers. For this reason, try not to use !important to override external styles. Instead, use @import together with the layer keyword or layer() function to import external stylesheets (from frameworks, widget stylesheets, libraries, etc.) into layers. Importing stylesheets into a layer as the first declaration in your CSS demotes their precedence, and author-defined layers, defined later in your CSS, will have higher precedence. The !important flag should only be used sparingly, if ever, to guard required styles against later overrides, in the first declared layer.

举个栗子🌰:

css 复制代码
@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)则不参与层叠,它们有自己的覆盖规则。别急,笔者马上介绍👉

Only CSS property/value pair declarations participate in the cascade. CSS at-rule descriptors don't participate in the cascade and HTML presentational attributes are not part of the cascade.

1、@声明的覆盖规则

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

For the most part, the properties and descriptors defined in at-rules don't participate in the cascade. Only at-rules as a whole participate in the cascade.

1.1 @font-face

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

css 复制代码
@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 整体,选出最适合的那个。如果有多个同样合适的,那么浏览器就会依次根据 来源和重要性 以及 出现顺序 来决定( @规则 没有特异性,所以不用考虑)。

For example, within a @font-face rule, font names are identified by font-family descriptors. If several @font-face rules with the same descriptor are defined, only the most appropriate @font-face, as a whole, is considered. If more than one are identically appropriate, the entire @font-face declarations are compared using steps 1, 2, and 4 of the algorithm (there is no specificity when it comes to at-rules).

1.2 条件性@规则

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

While the declarations contained in most at-rules --- such as those in @media, @document, or @supports --- participate in the cascade, the at-rule may make an entire selector not relevant, as we saw with the print style in the basic example.

1.3 @import

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

举个栗子🌰:

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

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

When it comes to @import, the @import doesn't participate itself in the cascade, but all of the imported styles do participate. If the @import defines a named or anonymous layer, the contents of the imported stylesheet are placed into the specified layer. All other CSS imported with @import is treated as the last declared layer. This was discussed above.

1.4 @charset

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

Finally, @charset obeys specific algorithms and isn't affected by the cascade algorithm.

1.5 @keyframes

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

CSS animations, using @keyframes at-rules, define animations between states. @keyframes don't cascade, meaning that at any given time CSS takes values from only one single set of @keyframes and never mixes multiple ones. If multiple sets of @keyframes are defined with the same animation name, the last defined set in the origin and layer with the greatest precedence is used. Other @keyframes are ignored, even if they animate different properties.

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

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

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

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

If the HTML presentation attribute is supported by the user agent, valid presentational attributes included in HTML and SVG, such as the align or fill attributes, are translated to the corresponding CSS rules (all SVG presentation attributes are supported as CSS properties) and inserted in the author stylesheet prior to any other styles with a specificity equal to 0.


总结

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

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

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

相关推荐
木木夕酱4 小时前
前端响应式网站编写套路
css·react.js
用户26834842239595 小时前
前端换肤功能最佳实践:从基础实现到高级优化
前端·css
蓝婷儿7 小时前
第二章支线八 ·CSS终式:Tailwind与原子风暴
前端·css
Java永无止境9 小时前
Web前端基础:HTML-CSS
java·前端·css·html·javaweb
超级土豆粉10 小时前
CSS 性能优化
前端·css·性能优化
Sun_light11 小时前
用原生 HTML/CSS/JS 手把手带你实现一个美观的 To-Do List 待办清单小Demo
前端·css·html
普宁彭于晏12 小时前
CSS3相关知识点
前端·css·笔记·学习·css3
Vinceri12 小时前
VSCode主题定制:CSS个性化你的编程世界
css·ide·vscode