从66%到185像素:浏览器在布局前如何解析CSS值
追踪 CSS 值从声明、层叠、指定、计算、使用到实际应用的各个阶段,了解为何相对单位以及 rem 转换技巧会呈现那样的行为。
赢得级联规则竞争并不意味着CSS声明的流程就此结束。即便width: 50%胜过了所有其他竞争规则,浏览器仍无法直接将50%传递给布局引擎;它必须先确定在当前上下文中“50%”对应什么,再将其转换为具体的尺寸值。本指南将跟踪某个数值在规范规定的各个处理阶段中的变化,展示rem等相对单位以及百分比是如何被解析的,并解释常用的像素转rem转换技巧及其在无障碍性方面的注意事项。
你输入的值并非最终使用的值
在样式表中输入的许多内容其实都是某种比例关系而非具体数值。宽度可以是父元素宽度的某一部分:
width: 50%;
或者在另一条规则中,是不同的比例:
width: 66%;
字体大小则可以相对于根元素的字体大小来设定:
font-size: 2rem;
高度可以是视口尺寸的一部分:
height: 50vh;
这些数值都不是以像素为单位表示的,但布局最终需要实际尺寸来确定每个元素的定位。为了解决这一矛盾,每个数值都要经过一系列固定的处理步骤:
Declared Value
↓
Cascaded Value
↓
Specified Value
↓
Computed Value
↓
Used Value
↓
Actual Value
这些名称乍看之下像是专业术语,但实际上每个步骤都在回答一个具体问题。以下内容将通过同一个实例依次讲解这些步骤。
声明值:你所要求的值
声明值就是出现在声明语句中的那个数值。给定:
width: 66%;
width的声明值为66%。这只是一个请求而已。同一个属性可以因不同的规则而拥有多个声明值,因此首要任务就是从中选择一个。
级联值:最终保留的声明
假设有两条规则都用于设置段落的宽度。其中一条使用类型选择器:
p {
width: 100px;
}
另一条则使用类选择器:
.amazing {
width: 66%;
}
如果某个段落带有amazing类,那么这两条规则都会匹配它。由于类选择器比纯元素选择器更具特异性,因此级联机制会选择:
width: 66%;
最终保留的66%数值就是级联值。这一步非常重要,因为后续的所有处理都只基于这个唯一的胜出者;那些被淘汰的声明会被直接丢弃,而不会被进一步处理。
指定值:确保每个属性都有值
指定值是指确保每个元素的每个属性都拥有某个值的成果。相关规则很简单:
- 如果存在级联值,那么它就会成为指定值;
在我们的示例中存在级联值,因此指定值仍然是66%,此阶段似乎没有起到任何作用。对于完全没有相关规则的属性来说,这一点就变得很重要了,而这正是继承机制发挥作用的地方。该机制将在最后一部分中详细说明。
计算值:在无需布局的情况下确定可确定的值
计算值是指浏览器在无需实际布局页面的情况下就能确定的所有数值。基于字体大小的相对长度会转换为绝对长度,而关键字则会转化为具体的数值。请看以下示例:
font-size: 2rem;
这里的单位是:
2rem
它描述的是一种“根元素字体大小的两倍”这样的关系,而非固定数值。在计算阶段,浏览器会获取根元素的字体大小并将其转换为像素值。如果根元素字体大小为16px,那么计算出的字体大小就是32px。
之所以在这一阶段进行转换,原因之一在于继承机制:计算出的数值会被子元素继承,这样它们就能获得具体的长度值,而非需要在其自身上下文中重新解读的关系表达。正因如此,嵌套的em字体大小才会以这种方式叠加效果,也解释了为何百分比设定的行高在字体较大的子元素中会导致文本显得拥挤。
某些值尚无法确定。百分比宽度取决于其所在容器的大小,而容器大小要在布局完成后才能知晓,因此 66% 在计算结果中仍为百分比形式,需在下一阶段才能最终确定。
实际使用的值:引入布局信息
实际使用的值是在获得布局信息后的结果。返回至:
width: 66%;
单独的百分比数值是不完整的,因为需要知道它所占比率的对象是什么。对于 width 来说,这个参照对象就是其所在容器的宽度。假设父元素为:
280px
较宽的容器。此时浏览器可以计算出:
66% of 280px
最终结果为:
184.8px
该数值即为实际使用的值。这是整个流程中的转折点:浏览器不再使用你设定的百分比,而是根据页面的实际几何尺寸得出真实尺寸。类似 auto 这样的宽度值也是如此,只有在知晓其周围的布局结构后才有意义。如果你使用 getComputedStyle 在 JavaScript 中查询某个元素,许多依赖于布局的属性(如 width)会返回经过计算的像素值而非百分比,这是在实际操作中观察这一过程的便捷方式。
实际值:适应设备的限制
最后一步需要考虑渲染环境的限制。设备并不一定能以任意精度来绘制数值,因此实际使用的值可能会是:
184.8px
最终可能以大致如下形式呈现:
185px
经过调整后的数值才是实际值。在现实中,现代浏览器会以亚像素精度进行布局,并根据属性和引擎的不同在不同环节应用四舍五入或对齐处理,因此不可依赖特定的四舍五入规则。关键点在于:计算出的数值与最终显示在屏幕上的数值之间可能存在最后的调整。
从更宏观的角度来看,整个过程可以归结为一句话:你在样式表中写的代码并不一定就是浏览器最终会使用的代码,其间存在一系列明确的处理步骤。
为何相对单位不再那么神秘
这一模型解释了为什么相对单位如此重要却又常常被误解。这类单位包括:
%
rem
em
vh
vw
允许你用其他参数(父元素大小、根字体大小、元素自身的字体大小或视口尺寸)来描述大小,而无需硬编码像素值。这正是让布局能够适应不同屏幕和用户设置的原因。
然而,在布局使用这些值之前,必须先根据其参考对象进行解析,且解析发生在不同阶段:字体相关单位在计算阶段进行解析,百分比则在使用阶段根据所在容器块进行解析。考虑到这一处理流程,你可以将“浏览器会自行处理”这种模糊的概念,替换为调试时更具体的问题:该值依赖于什么?当前的参考对象又是什么?当响应式布局出现异常时,问题往往出在参考对象与预期不同,比如百分比所依据的容器块并非预想中的那个,或是em单位经过多个嵌套元素后产生了累积效应。
以10px为基准将px转换为rem
理解数值处理过程也有助于明确一种常见的尺寸设置流程:将设计中的像素值转换为rem单位。假设某项设计规定了:
padding: 30px;
而你更希望以如下方式表达它:
padding: 3rem;
一种常见的做法是将根字体大小设置为整数:
html {
font-size: 10px;
}
确定这一点之后:
1rem = 10px
数值转换就变成了简单的心算:
30px → 3rem
40px → 4rem
20px → 2rem
总体而言:
rem = pixels / 10
其好处不仅在于数值更加整齐。由于每个rem值都是在计算阶段根据根字体大小确定的,因此只需修改这个根值,页面上所有基于rem的尺寸都会同步调整,从而实现对整个尺寸系统的统一控制。
有一个重要的注意事项。将根值设置为固定的 10px 会覆盖用户在浏览器设置中选择的默认字体大小,这违背了最初使用 rem 的主要无障碍设计目的之一。一个广泛使用的替代方案是 html { font-size: 62.5%; }:在浏览器默认的16px设置下,它仍然表现为10px,但当用户调整自己偏好的字体大小时,它会按比例缩放。如果采用这种方案,请记得在 body 上设置一个便于阅读的字体大小(例如 1.6rem),否则文本将会默认使用较小的根值大小。
完全没有声明任何设置时
上述步骤仍有一个问题没有得到解答。以以下标记为例:
<div class="parent">
<p>Hello World</p>
</div>
以及包装元素上的规则:
.parent {
color: red;
}
目前还没有类似这样的规则出现:
p {
color: red;
}
然而该段落的文字却是红色的。由于该段落没有级联的 color 设置,其指定的颜色值必须来自其他地方。因为 color 是一个继承属性,浏览器会使用父元素的计算后的颜色值。这就是继承机制,它直接对应于前面提到的指定值阶段。要详细了解哪些属性会继承、为什么子元素会获得计算后的值而非声明的值,以及 inherit 和 initial 如何覆盖默认值,就需要专门探讨继承机制。
关键要点
- CSS 值会经历六个阶段:声明、级联、指定、计算、使用和实际值。
- 级联机制会将许多声明的值简化为单一的级联值,只有这个胜出的值会继续被处理。
rem和em这样的基于字体的单位在计算阶段会转换为绝对长度;百分比宽度则需等待布局完成,在实际使用阶段才会确定。62.5%比固定的10px更能体现用户的字体偏好设置。