当设置为“无”时CSS值从何而来:继承机制是如何工作的
了解浏览器如何判断属性是否继承、子元素为何会获得父元素的计算值,以及 inherit 和 initial 如何让您实现精确控制。
来源: How Inheritance Works in CSS Internally at Algorithmic Level.
你在容器上设置了color属性,尽管没有任何规则提及它,容器内的段落仍会变成红色。而你为同一个容器设置的内边距则保持原位不变。这两种现象都源于CSS的继承机制,即当元素没有自身属性值时,系统会用父元素的值来填充。本指南将解释哪些属性会继承、具体会传递什么值(当涉及百分比时,答案常常让很多人感到意外)、inherit和initial关键字如何改变默认行为,以及继承机制在将样式表转换为最终值的过程中所处的位置。
未被声明的值
首先从父元素上的规则开始:
.parent {
color: red;
}
以及该父元素内部包含段落的标记:
<div class="parent">
<p>Hello World</p>
</div>
虽然没有规则为<p>设置color属性,但其文本仍显示为红色。当浏览器处理到这一部分时,它已经通过级联规则解决了各种冲突的声明,并处理了相应的值。此时需要为每个元素的每个属性决定其值应来自元素自身的声明还是来自层级结构中更上方的部分。继承机制就是用来做出这种决定的。
哪些值会沿层级结构向下传递
继承机制允许某些属性值从父元素传递给子元素,再由子元素继续传递给它们的后代:
Parent
↓
Child
↓
Grandchild
并非所有属性都会参与继承。用于描述文本的属性就是典型的可继承属性,例如:
font-family
font-size
color
这就是为什么很少需要在每个元素上重复设置字体样式的原因。不必在上设置颜色,然后再为每种子元素单独设置:
body {
color: #333;
}
h1 {
color: #333;
}
p {
color: #333;
}
li {
color: #333;
}
你可以仅在文档结构的顶层声明一次,让继承机制将其传递下去。这样不仅能减少CSS代码量,而且在设计变更时只需在一个地方进行修改。body或:root上设置基础字体和颜色就是这一理念的常见应用。
浏览器对每个属性的决策方式
当某个元素没有对应的样式声明时,其逻辑会更加清晰。对于每个属性,浏览器实际上会依次询问两个问题。
首先:是否存在级联值?如果该属性的任何声明在此元素上赢得了级联优先权,就会使用该值,继承机制便不会起作用。
其次,仅在答案为“否”时才需判断:该属性是否可继承?这完全取决于具体属性,因为规范中规定了每个属性是可继承还是不可继承。以下是一些可继承属性的示例:
color → inherited
font-family → inherited
font-size → inherited
以及一些不可继承的属性:
margin → not inherited
padding → not inherited
这种设计是刻意为之的。试想一个margin属性会继承的世界:某个区域设置的单一外边距会被复制到该区域内的所有段落、列表和链接上,从而导致各级间的间距不断累积。像外边距、内边距、边框和宽度这类与盒子相关的属性,几乎总是需要为每个元素单独设置;而与文本相关的属性则通常希望在整个区域内统一应用。如果某个属性不继承且没有级联值,它就会采用该属性的初始值,即为其定义的默认值。
如果您对某个特定属性不确定,其参考文档中的“是否继承”一栏就能直接给出答案。
子元素继承的是计算后的值,而非您输入的文字
这是整个过程中最重要的细节,也是某个著名“惊喜”现象的根源。假设父元素设置了字体大小和百分比形式的行高:
.parent {
font-size: 20px;
line-height: 150%;
}
而子元素仅覆盖了字体大小:
.child {
font-size: 25px;
}
那么子元素的行高最终会是多少?直观的猜测是子元素会直接使用那个百分比值:
150%
然后再根据自身更大的字体大小来调整它:
150% of 25px
实际并非如此。元素继承的是父元素的计算后的值,而非最初声明的值。对于line-height而言,百分比值会在计算时被转换为具体长度。在父元素中,其计算公式为:
150% of 20px = 30px
因此父元素的计算后行高为:
30px
正是这个绝对长度会被传递下去。尽管子元素使用了更大的字体,但它继承的行高仍然是:
30px
而不是:
150% of 25px
实际后果是,当子元素使用的字体比定义百分比的元素更大时,行高会显得十分拥挤。对于line-height的em值也是如此,因为它们在继承之前也会被转换为长度。这正是为何通常建议使用无单位的行高值如line-height: 1.5:纯数字本身就是计算出的值,这样每个子元素都会根据自身的字体大小来乘以该数值,而非继承一个固定的长度。
不继承的属性会恢复为初始值
现在来看另一组中的某个属性:
padding
内边距不会继承。因此当父元素声明时:
.parent {
padding: 20px;
}
子元素不会自动获得该属性值:
padding: 20px;
由于没有级联传递也没有继承机制,子元素的内边距会使用该属性的初始值,而对于内边距而言,其初始值为:
0px
这种情况会一直保持,除非有其他规则明确为子元素设置不同的内边距值。了解某个属性属于哪一类,有助于在调试时节省时间:如果某个值似乎“泄露”到了子元素中,那它就是可继承的属性;如果它在子元素中似乎“消失”了,那它就是不可继承的属性。浏览器开发者工具可以提供帮助,因为“计算”面板能够显示继承来的值及其来源的父元素。
使用 inherit 强制实现继承
你可以为任何属性覆盖默认行为。inherit关键字会指示浏览器即使对于通常不会继承的属性也采用父元素的值:
.child {
padding: inherit;
}
通过这条规则,子元素的内边距会与父元素计算出的内边距保持一致。当需要明确体现父子元素之间的关联时,这一功能非常有用,例如让嵌套元素复用其容器的边框颜色或间距,或者使用font: inherit强制表单控件采用周围的字体样式,因为这些控件在浏览器的默认样式表中通常不会继承字体设置。
这为获取父元素的值提供了两种不同的方式。自动继承仅适用于继承的属性,且仅在不存在级联值时生效。inherit实现的显式继承本身也是一种声明,因此它会像其他任何值一样在级联中占据优先,并且适用于所有属性。
使用initial重置
相关的关键字是:
initial
将其应用于某个属性时,该属性会被重置为规范中定义的初始值:
.child {
color: initial;
}
请注意,“initial”指的是规范中的默认值,而非浏览器的默认样式表或元素原本会继承的值。对于color属性,其初始值为系统文本颜色(通常是黑色),因此color: initial并不会恢复父元素的颜色,反而会使该元素与父元素分离。对于display属性,其初始值为inline,这就是为什么在div上使用display: initial会使其变为内联元素而非恢复为块级元素的原因。
简而言之,这两个关键字的作用是完全相反的:
inherit → use the parent's value
initial → reset to the property's initial value
这些功能共同让开发者能够精确控制值在样式树中的传递方式。CSS还提供了unset(对于继承的属性其行为类似于inherit,而对于其他属性则类似initial)和revert(会将样式恢复为浏览器的默认样式表),在重置组件时,这两者往往比initial更为实用。
全面了解样式继承机制
将样式继承与之前的处理步骤结合来看会更有帮助。当有多条规则作用于同一个元素时,浏览器会按照级联规则依次比较这些规则的声明:
Importance
↓
Specificity
↓
Source Order
最终确定的值即为级联后的值。该值会经过一系列处理阶段,直到变成布局和渲染能够使用的形式:
Declared Value
↓
Cascaded Value
↓
Specified Value
↓
Computed Value
↓
Used Value
↓
Actual Value
当不存在级联值时,继承机制会为该序列提供输入:
No Cascaded Value
↓
Can Property Be Inherited?
↓
Yes
↓
Parent's Computed Value
↓
Child
严格来说,规范规定继承发生在指定值确定的时刻:如果有级联值,则该指定值为级联值;否则为父元素中继承属性的计算值;再不然则为初始值。之后便是常规的计算、使用和实际值这三个阶段。因此,CSS处理阶段的简化整体结构如下所示:
CSS
↓
Cascade
↓
┌─────────────┴─────────────┐
↓ ↓
Importance Specificity
↓
Source Order
↓
Cascaded Value
↓
Value Processing
↓
Computed / Used / Actual
↓
Inheritance
↓
CSSOM
应将该图中的垂直顺序视为概念的组合,而非严格的时间线,因为继承机制是直接赋予指定值,而非在计算出数值之后才起作用。无论从哪种角度理解,核心思想都是一样的:在样式表与屏幕呈现之间发生的复杂过程远不止“我写了一条规则,浏览器就应用了它”这么简单。
从样式表到像素
退一步来看,以下是扩展了CSS处理阶段的完整流程:
HTML
↓
DOM
↓
CSS
↓
Cascade
↓
Specificity
↓
Value Processing
↓
Inheritance
↓
CSSOM
↓
DOM + CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels
考虑到这一整个处理流程,即便是一条简单的规则也蕴含着许多隐含的工作:
.title {
font-size: 2rem;
color: blue;
}
在开始绘制任何内容之前,浏览器必须先确定:
- 哪些声明适用于每个元素;
- 当这些声明发生冲突时,哪条会优先生效;
- 相互竞争的选择器具体有多精确。
2rem相当于多少像素);只有完成所有这些步骤后,页面才会进入布局和渲染阶段。
CSS作为系统而非规则列表
一旦不再将CSS视为孤立的规则,而是将其看作一个具有明确职责的完整系统,很多关于CSS的困惑就会消失:
- 特异性解释了为何某个声明会优于另一个。
- 来源顺序在特异性相同时用于解决冲突。
- 值处理则将你输入的内容转换为浏览器能够使用的实际值。
它们共同将样式表转化为一套完整的最终数值,这些数值用于布局和渲染。即便只是像这样简短的声明:
width: 66%;
也会经过所有这些处理步骤。浏览器会确定适用哪条规则,处理百分比值,将其与包含块关联起来,最终确定宽度,而这一切都在第一个像素显示之前完成。
之后剩下的则是另一个问题:每个元素究竟应放置在哪里。宽度、高度、正常流、定位以及盒模型都属于视觉格式化模型,这是处理流程中的下一阶段,它将这些最终数值转化为实际的布局。
关键要点
- 属性的数值首先来自级联规则;只有在没有相应声明时才会考虑继承机制。
color、font-*)或不可继承的(多为与盒子相关的属性:margin、padding)。em单位表示的行高会以固定长度的形式传递下去;建议使用无单位的line-height。padding的初始值为0。inherit强制所有属性都可继承;initial会将属性值重置为规范中的默认值,而非父元素的值;unset和revert通常是更合适的重置方式。