CSS的层叠和继承

在一个较大的网站中,可能会存在成百上千个 HTML 元素。每一个元素的每一种样式必须应用了合适的值,网站才能表现出正确的显示效果。为此,开发人员需要编写大量样式,这就会使得样式表变得很复杂。

CSS 提供了几种机制:层叠、继承和初始值,帮助开发者尽可能减少编写规则的负担,并解决规则之间的冲突问题。这些机制是 CSS 中的核心概念,也是用户代理在渲染元素的关键步骤。接下来的三节分别讨论这三种机制。

层叠

在一个较大的样式表中,可能会有很多条规则都作用于同一个元素的同一个属性中。例如:

<main id="main"> <article> <div class="content"> <h1>Understanding The Cascade</h1> <p>You will often ... style the element.</p> </div> </article> </main>
main .content > p { color: red; } main article div p { color: orange; } #main p { color: green; }

article .content h1:first-child + p { color: blue; } p { color: purple; }

这些选择器都作用于示例中的 <p> 元素,并且应用的是相同的属性。那么该元素到底应该显示为什么颜色?这就涉及到声明冲突这个问题,即同一个样式被多次应用到同一个元素上的情况。

为此,CSS 提出了层叠(cascade)的概念,“层叠”指的是当多个样式规则应用于同一个元素而发生声明冲突时,如何决定哪条规则最终生效的过程。 层叠的机制比较复杂,但同时也非常重要,毕竟 CSS 的全称就是层叠样式表。

选择器的特殊性

CSS 会为每一个声明赋予一个权重,称为特殊性(specificity, 也翻译为特定性、特指度等),用于确定哪个声明的优先级更高。

声明的特殊性和声明所使用的选择器有关,具体表现为如下所示的 4 个数的组合:

(?, ?, ?, ?)

层叠样式的优先级由这 4 个数字确定,这 4 个数字从前到后的含义为:

  1. 是否为内联样式
  2. 包含 ID 选择器的数量
  3. 包含类选择器、伪类选择器、属性选择器的数量
  4. 包含元素选择器和伪元素选择器的数量

在确定了这四个数字以后,用户代理会对冲突的规则比较这 4 个数字,具体方式为:先比较第一个数字,数字大者胜出;在一样大的情况下,再比较第二个数字,以此类推。

特殊性比较有一个总体规则:选择器选中的范围越窄,特殊性越高

内联样式(即 style 属性内的样式声明)虽然没有选择器这一概念,但它只影响单一元素,具有最高的优先级,可以覆盖样式表内的任意规则。因此,直接向元素的 style 属性添加样式可以保证样式一定生效(浏览器的开发者工具就是这样做的,但这种做法应该尽可能避免)。

对于不是内联样式的其它情况,ID 选择器往往只选择局部元素,因此它对特殊性的影响最大,其次是类选择器,最后是元素选择器。因此,在编写选择器时,应该尽可能只用元素选择器和类选择器就能达到选择要求。

以上几条规则对应的选择器特殊性如下(按特殊性从高到低排序):

#main p { / specificity: (0, 1, 0, 1) / } article .content h1:first-child + p { / specificity: (0, 0, 2, 3) / } main .content > p { / specificity: (0, 0, 1, 2) / } main article div p { / specificity: (0, 0, 0, 4) / } p { / specificity: (0, 0, 0, 1) / }

因此,元素最终会表现为第一个选择器对应规则的样式,即文本颜色为绿色。

除此之外,还有一些其它常见情况的特殊性规则如下:

  • 通配选择器 * 的特殊性是 0 ,也就是说再多的通配选择器都不会改变特殊性。任何不只有通配选择器的规则都会覆盖只有通配选择器的规则。
  • 所有的后代选择器或兄弟选择器只是组合多个选择器用的,它们本身不具有特殊性
  • 对于使用 , 分隔的多组选择器,每个分组的规则是独立的,选择器的特殊性也是独立计算的

重要性声明

除了特殊性,另一种影响样式优先级的方式是重要性声明。重要性声明即在某一条样式后面加上 !important ,表示这是一个重要样式,例如:

p.warning { color: red !important; font-size: 16px; }

这里对 color 属性应用了重要性声明,!important 必须紧跟在样式属性值的后面,放在这条属性声明的末尾。重要性声明会会打破常规的层叠顺序,覆盖所有没有重要性声明的规则,甚至可以覆盖没有重要性声明的内联样式。

虽然 !important 提供了一种直接覆盖其他样式规则的方法,但它并不是应该经常使用的工具。因为重要性声明的优先级太高,后续想要覆盖这个样式,只能再次使用重要性声明,可能导致整个样式表充满了 !important ,显著降低代码的可维护性。如果所有规则都使用了 !important ,它就失去了意义,最终还是要依赖选择器的特指度和声明顺序来确定优先级。

因此,在日常编写样式时,还是推荐通过提高选择器的特殊性、合理设计样式层次等方式解决样式冲突,而非直接依赖 !important

不过,对于以下情况,使用重要性声明是比较合理的:

  • 在难以确定样式来源,或无法直接修改来源的样式(如使用的是第三方样式,或需要覆盖内联样式)时,使用重要性声明可以帮助确保特定样式生效。
  • 需要临时或快速解决某些样式冲突时,例如在调试项目时,需要快速判断应用某些样式后能否实现效果,可以暂时通过重要性声明覆盖现有样式。

来源次序

最后,在特殊性相同的情况下,会以在源代码中出现的先后位置为准:来源靠后的规则会覆盖来源靠前的规则。这一步一定能解决冲突,因为代码总有先后顺序。

具体来说,CSS 的规则有以下来源:

  • 用户代理样式:即浏览器的默认样式。用户代理样式可以使页面没有提供任何 CSS 时为元素提供不同的基础样式,使页面保持一定的可读性。
  • 用户样式:这是浏览器的用户自己定义的样式表,目前仅有部分浏览器支持。用户可以通过自定义样式表来覆盖网站的样式,通常用于提高可访问性,例如增加字体大小、改变页面背景等。
  • 作者样式:即页面的开发者为页面定义的 CSS 样式。作者样式又根据其引入方式,分为内联样式、内部样式和外部样式。

当多个样式来源产生冲突时,将按照如下的顺序层叠:(按优先级从高到低)

  • 用户代理样式中的 !important 样式
  • 用户样式中的 !important 样式
  • 作者样式中的 !important 样式
  • 作者样式
  • 用户样式
  • 用户代理样式

注意一个细节:没有重要性声明的样式,作者样式优先级最高,而用户代理优先级最低;而有重要性声明的样式,优先级是相反的。

一般情况下,用户代理样式的 !important 很少见,因为它会导致用户和开发者都无法覆盖。

对于同一种来源的样式,则取决于在源代码中出现的位置:在源代码中靠后声明的样式会覆盖靠前的样式。对于外部加载的样式(如通过 <link> 标签和 @import 规则引入的样式),则视为将包含的规则放在加载它的位置,同其它规则比较顺序。

注意,这里的表述是“出现的位置”或者说是“书写的顺序”,而不是样式实际加载的顺序。对于异步加载的样式,仍然以它们加载完后在源代码中出现的顺序为准。

这个规则说明,在编写网页的样式时,样式的书写顺序或文件引入的顺序是有一定要求的:一般来说来自第三方的样式应该写在最上面,或者最先引入;其次是定义网站基础效果的样式;针对某个组件或主题的样式则放在最后。

SMACSS

SMACSS(Scalable and Modular Architecture for CSS) 不是一个具体的样式表,而是一种用于组织和编写 CSS 的方法学。SMACSS 的目的是帮助开发者更有条理地编写和管理 CSS 代码,使样式更加模块化、可维护,并适应大型项目的复杂性。

SMACSS 的完整内容可以在官方网站找到。SMACSS 建议将 CSS 规则分为以下几类:

  1. 基础(Base):每种 HTML 元素的默认样式,通常用于为所有页面提供一致的基本显示效果(例如网站的字体和字号、段落和链接的文字颜色等)。
  2. 布局(Layout):定义页面的主要结构。通常页面会被分为几个主要区域(例如页头、页脚、内容区、侧边栏等),布局样式只负责这些区域的结构和排版,而不涉及区域内的元素样式。
  3. 模块(Module):用于为页面中可复用的独立组件设置样式(例如按钮、卡片、表单、导航栏等)。这些组件应该可以在不同的页面或页面中的不同区域复用。
  4. 状态(State):表示组件或布局在不同状态下的样式(例如悬停、选中、禁用等)。状态通常通过类名来控制,后续可以考虑结合脚本来实现动态的状态切换。
  5. 主题(Theme):控制页面的整体视觉风格(如文字颜色、背景等)。有些网站支持不同主题的切换,即便没有这个要求,在设计时最好也要有这种意识。

通过以上顺序组织 CSS 代码,不仅可以使样式的结构更清晰,同时也降低了样式发生不合理地覆盖的可能。

LVHA规则

针对链接元素 <a> 的伪类一共有 4 个:

  1. :link(普通、未访问状态)
  2. :visited(已访问状态)
  3. :hover(光标悬停状态)
  4. :active(激活状态)

对于链接的四种状态,如果想让它们正常工作,样式声明的顺序应该和以上介绍的顺序一样。这是因为这 4 个伪类的特殊性相同,但是它们与用户交互的顺序不同。果不按照这里列出的顺序声明样式,普通的状态可能会覆盖特殊的状态,导致无法显示预期结果。

这就是针对链接常用的 LoVe-HAte(LVHA) 规则。LVHA 规则确保了不同状态的链接样式不会互相覆盖,按用户交互顺序展现正确的视觉效果。

继承

当为元素编写 CSS 属性的时候,有些时候需要为页面的某个部分设置一些统一的样式。例如,对于以下段落,一种常见的需求是将整个段落的文字设置为某个颜色:

<p><code>:hover</code> <strong>pseudo-class</strong> only applies if the user moves their pointer over an element, typically a <a href="#link">link</a>.</p>

按照通常的想法,如果想要将这整个段落的所有文字改变颜色,那么不但要对 <p> 元素应用字体颜色,还要为 <p> 中的其它行内元素应用字体颜色,才能保证整个段落的样式统一。

实际上,CSS 提供了一种机制避免这种繁琐的操作:CSS 中的祖先元素也会向后代传递一样东西:CSS 属性的值。这种规则称为继承(inheritance)。继承是指一个元素可以自动获得其父元素某些 CSS 属性的值。例如,以上文档片段只要这样应用样式:

p { color: chocolate; }

那么,<p> 元素内的子元素 <code><strong> 等就会获得同样的 color 属性,使整个段落获得一致的视觉效果。

继承机制使得样式不仅应用到指定的元素,还会应用到它的后代元素,包括后代元素的后代元素。由于一个元素往往具有不止一个祖先元素,因此继承也可能发生冲突:如果在继承时引发了冲突,则由最近的祖先元素决定继承结果。

CSS 中有很多属性是可以继承的,其中相当一部分都跟文本有关,比如颜色、字体、 字号。然而,也有很多 CSS 属性不能继承,这些不能继承的属性主要涉及与背景和定位相关的属性(例如边框和边距),因为这些属性会造成内外元素外观上的冲突,或是打乱元素的布局,因此继承这些属性是没有意义的。

注意:继承和特殊性是两个概念,继承的值根本没有特殊性,连 0 特殊性也没有。任何出现在样式表的规则都能覆盖继承的样式。虽然通配选择器的特殊性为 0 ,但它毕竟是出现在样式表的规则,因此通配选择器能覆盖继承的属性,使用通配选择器是一种禁止继承的方法。

利用继承机制,可以帮助减少代码冗余,并使得代码易于维护:只需在区域的顶级元素上定义一次属性,区域的所有子元素就会自动获得这些样式。因此,继承很适合用在修改列表、表格、页脚这些具有结构复杂,但样式相对统一的元素上,简化了编写选择器的过程。对于以上文档结构及样式,在浏览器的表现效果为:

可以看到链接并没有变为红色,这是因为用户代理的默认样式表有为 <a> 标签设置了字体颜色,因此 <a> 标签并没有被继承影响。

用户代理为了能在不引入任何样式的时候大致展现文档的内容和结构,一般会提供一张默认的样式表,给元素赋予少量的样式,例如:

  • 段落元素 <p> 、标题元素 <h1>~<h6> 有上下间距
  • 强调元素 <strong> 、标题元素 <h1>~<h6> 有字体加粗
  • 列表元素 <ul><ol> 有左侧间距使列表项缩进
  • 行内的 <em><var> 有字体倾斜
  • ……

但是,不同的浏览器中,提供的默认样式表的规则也有所不同。例如,标题元素的字体大小和粗细可能不同;列表元素的缩进方式和列表项的样式可能不一致;表单控件(如按钮、输入框等)在不同浏览器上的显示效果甚至布局方式都有很大差别。如果不考虑这些默认样式,页面可能会在不同的浏览器中的显示效果出现一些偏差。

为了消除默认样式表带来的影响,可以使用 CSS reset 技术。CSS reset 将所有常见 HTML 元素的默认样式清除,让开发者在一个一致的基础上设计样式,确保网页在不同浏览器中呈现一致的基础外观。

网络上现成的 CSS reset 方案很多,其中一个比较流行的方案是 Eric Meyer's CSS Reset。它的代码相对简单,容易理解和修改,可以直接复制到本地项目中使用,也可以通过 CDN 引用。

CSS normalization 是一种更加温和的方案,它并不是直接重置所有的默认样式,而是在保留一些有用默认样式的基础上,统一浏览器不一致的样式。normalize.css 是一个比较流行的实现,这里是 normalize.css官方网站GitHub 仓库链接。

考虑到 CSS 书写顺序对优先级的影响,CSS reset 代码应该放在其它任何 CSS 代码之前,因为引入 CSS reset 的用意是只重置默认样式表中的样式,而不是开发者的样式。由于作者样式表优先级高于默认样式表,因此放在首位是最合理的。

由于显示在页面上的元素都位于 <body> 元素内,因此可以通过为 <body> 应用规则来统一整个文档的基本样式,例如:

body { font-family: Helvetica, Arial, sans-serif; color: #131313; }

那么文档中的所有元素都将继承这些样式,无需在每个标签上分别指定。对于个别想使用不同字体的元素,只需要单独在为这个元素设定 font-family 属性即可。

初始值

初始值(initial value)在很多情况下是确保页面正常显示的关键。当元素在页面上显示的时候,它的每一个属性都需要有对应的值,用户代理才知道这个元素最终如何显示。但是在编写样式的时候,不可能为每一个元素都指定所有的属性,这样显然过于繁琐了。

因此,CSS 提出了初始值的概念。初始值是指每个 CSS 属性在没有指定值或继承值时的默认值,确保每个属性在没有明确得到值的情况下仍然具有一个基础的默认表现。

每个属性对应的初始值都不尽相同,它们由 CSS 规范定义,可以在需要时查阅。以下列出了几个常见属性对应的初始值:

  • color :初始值取决于用户代理,通常为黑色 #000000
  • background-color :初始值 为 transparent
  • border-radius :初始值为 0
  • font-size :初始值为 medium
  • font-family :初始值取决于用户代理
  • line-height :初始值为 normal

不要混淆了继承和初始值。继承是指某些样式可以从父元素传递到子元素,而初始值则是当元素缺少某个属性的定义时,用户代理提供的预设值。不是所有属性都能继承,但是所有属性都会有初始值。当属性既没有指定值,也不能通过继承得到值的时候,它就会采用预设的初始值。

CSS 定义了几个和继承与默认值相关的关键字值,它们可以应用于任意属性:

  • inherit

关键字 inherit 可以使元素某个属性强制继承父元素对应属性的值。虽然大多数文本相关的属性都会自动继承,但是还有很多属性是不会继承的。此时就可以使用 inherit 值来确保子元素和父元素的样式一致。

例如,border-radius 属性不会自动继承,如果要使子元素和父元素有相同的圆角边框,这时就可以使用 inherit 。还有一些时候,子元素的样式被其它规则修改了(例如用户代理会将 <a> 标签的颜色设置为蓝色),此时想将它的颜色重置为继承父元素的文本颜色,这时也可以使用 inherit

  • initial

关键字 initial 把属性的值重设为默认值。例如,font-weight 属性的默认值是 normal ,那么 font-weight: initial 的作用与 font-weight: normal 一样。

initial 关键字看似有些多此一举,但要知道不是所有属性的初始值都是一个明确的值。例如,font-family 属性的初始值取决于用户代理,甚至可能最终取决于用户所使用的操作系统。此时开发者连用户代理有哪些可用的字体都未必知道,那么就非常适合使用 initial 关键字。

  • unset

unset 是 CSS3 引入的一个新关键字,表示“重设”样式,可以看作 inheritinitial 的结合:对于能继承的属性,unset 的作用与 inherit 相同;对于不能继承的属性,unset 的作用与 initial 一样。

总结

属性值的计算过程

用户代理渲染页面是一个复杂且关键的流程,它决定了页面内容如何从 HTML、CSS 和 JavaScript 等源代码最终呈现给用户。这个流程可以简单地概括为以下几个步骤:

  1. 解析 HTML 和 CSS ,生成对应页面的元素及其层级关系的树状结构
  2. 构建完整的渲染树,包含需要显示到页面上的每个元素及每个元素确定的样式
  3. 通过渲染树布局相关的属性,计算每个元素的位置和形状等几何信息
  4. 布局确定后,继续确定其它需要绘制的内容,例如文本、颜色、背景和边框等
  5. 最后,用户代理会将这些内容组合起来,显示在用户的屏幕上

渲染每个元素的前提条件是元素的所有的 CSS 属性都必须有一个确定的值,本节介绍的层叠、继承和默认值都涉及到属性值的计算过程。以下总结了属性值的计算过程。

属性值的计算过程包括以下四个步骤:

确定声明值

在页面的加载阶段,用户代理会找到所有的样式表,记录样式表的每一条规则声明,包括声明所使用选择器作用的元素范围。

处理层叠

规则之间往往会存在冲突的声明,这时候就需要根据规则决定最终哪一条规则生效。层叠又可以分为以下几个小步骤:

  1. 比较重要性

带有重要性声明 !important 的样式比不带重要性声明的优先级更高。

  1. 比较特殊性

如果几条冲突的样式都带有重要性声明或都不带重要性声明,那么就会比较它们的特殊性,特殊性更大的优先级更高。

这一步骤中,内联样式的优先级会比在样式表中的样式更高。

  1. 比较源次序

如果几条冲突的样式特殊性都相同,那么就会比较它们的来源顺序。

对于不同来源的样式,则:

  • 对于没有重要性声明的样式,则优先级为:作者样式优先级最高,用户样式次之,用户代理样式最低;
  • 对于有 !important 声明的样式,则优先级顺序相反。

对于同一种来源的样式,则比较它们在源代码中出现的位置:越靠后的样式优先级越高。

至此,到了这一步一定能解决规则的冲突。但是规则冲突只是属性值计算过程中的一步,对于没有声明直接声明的属性,还需要继续执行以下步骤。

继承属性值

如果在前两个步骤后,某些属性值仍未确定,且该属性可以继承,则会从父元素继承属性值。 当然,某些属性是不能继承的,如 background-color。这意味着,即使父元素定义了背景颜色,子元素仍需要单独指定或使用默认值。

使用初始值

对于仍然没有确定值的属性,浏览器会使用该属性的初始值,用于确保页面可以正常显示。例如,background-color 的默认值是 transparent,表示透明。如果在样式表中未指定某个元素的背景颜色,浏览器就会将其背景设为透明。

至此,所有的属性都必定能得到一个计算后的值,用户代理会根据这些属性将元素绘制到页面上。

以上就是 CSS 层叠、继承和默认值的具体内容。虽然这些过程都是由用户代理自动处理的,但了解这个过程,对于理解和解决样式冲突、编写可维护的 CSS 代码而言是非常重要的。

参考资料/延伸阅读

Cascade, specificity, and inheritance - Learn web development | MDN

CSS Cascading and Inheritance Level 3 - W3C

京ICP备2021034974号
contact me by hello@frozencandles.fun