前言
不管通过哪种渠道学习 CSS,CSS 三大特性(层叠、优先级、继承)永远是是基础中的基础,都会被放在最前面的课程学习。我查阅过一些关于讨论三大特性的资料,包括视频、文章等格式,很少有人会提到它们在标准中是如何定义的,特别是一些机构的视频,从最终的效果来看,这并不会影响学习 CSS 开发。对于初学者而言,一些具有总结性带有经验的话语确实比标准中的定义更好理解。
现在,不管你的水平如何,假设你正在进行一场面试,面试官(没错就是我)对你说:谈谈对 CSS 的三大特性的理解。不知你会如何作答?即使是一名刚接触 CSS 的开发者,也清楚它们最终的效果。但是或许你并不知道它们在规范中是如何定义的,它们之间到底是如何联系在一起的,哪些信息是我想从你的回答中听到的?
如果你像我一样,对标准中的定义有着执念,又或者你也想知道标准中是如何讲解三大特性的,那么这篇文章会告诉你答案。我并不是说规范中的定义是这个问题的唯一答案,只是在了解其概念后或许你会有更深的理解,不仅仅是为了面试,更多的是为了概念本身,更好的了解 CSS 世界。
本篇的主要内容有:
- 层叠的定义、具体规则、结果/输出,层叠值;
- 层叠、优先级、继承如何抉择出最终的 CSS 规则(处理流程);
- 层叠排序、优先级算法分别解决了什么问题;
- 继承的属性值是如何获得的;
- 各种 CSS 值(指定值、计算值、应用值等)的定义、计算方式;
注:参考资料来自 mdn 和 w3c,文章最后会放上链接。
预备知识
这里有一些你需要提前知道的概念。
CSS 声明、声明值、有效的声明
关于 CSS 的组成,mdn 描述如下:
在最基本的层面上,CSS 由两个组成部分组成:属性和值。当一个属性与一个值配对时,这种配对被称为 CSS 声明。
警告: 如果属性未知或某个值对给定属性无效,则声明被视为无效,并被浏览器的 CSS 引擎完全忽略。
组成 CSS 声明的属性值就是声明值 declared value ,这里想要强调的是无效的 CSS 声明会被忽略。
关于有效的 CSS 声明 w3c 给出过一段描述:
A declaration applies to an element if:
- It belongs to a style sheet that currently applies to this document.
- It is not qualified by a conditional rule with a false condition.
- It belongs to a style rule whose selector matches the element.
- It is syntactically valid: the declaration's property is a known property name, and the declaration's value matches the syntax for that property.
你可以简单理解为:能让浏览器解析出效果(效果不一定是可视的)的 CSS 声明就是有效的,对于不清楚的声明可以借助浏览器开发工具,可以很清楚的看出声明在哪里出现了问题:
html
<style>
p {
/* red 是 color 属性的属性值,也是声明值 */
color: red;
/* 无效的 CSS 声明会被忽略 */
font-size: red;
my-property: 12px;
}
</style>
源
源是指 CSS 声明的来源,即该声明是从哪里的。这里讲两点:源的种类、源的特点重要性。
源的种类
CSS 定义了三种核心的源:
- 用户代理样式表:指浏览器的样式表,不可修改。浏览器会有一个基本的样式表来给任何网页设置默认样式。一些浏览器通过使用真正的样式表,而其他则通过代码模拟,但无论是哪种情形都应是不可被检测的;
- 用户样式表:作为浏览器的用户,可以使用自定义样式表定制使用体验;
- 作者样式表:网页开发者定制的样式表;
用户代理样式表(浏览器默认样式)给元素设置的默认效果也属于 CSS 声明,通过浏览器开发工具可以看到声明来自哪个源以及没有生效的源。
对于初学 CSS 的开发者而言源或许不好理解,你需要知道的就是:浏览器有一个自带的样式表,称为用户代理样式表,这个样式表里的代码也属于 CSS 声明(浏览器写的 CSS 代码)。在我们没有写任何的 CSS 代码时,一些元素依然具有默认样式,就是因为该表的设置。不同浏览器的实现的方式不同,但是对于开发者而言,效果是相同的。比如 <a>
元素,字体颜色为蓝色、带有下划线等默认的样式,下面截图是 edge 浏览器实现的方式。
w3c 上还写到 CSS 的扩展定义了另外两种源:
- 动画源 Animation Origin
- 过渡源 Transition Origin
关于这两点的描述都是:通过虚拟的规则表示它们正在运行的效果。
源的重要性
源有一个重要的特点就是重要性 importance ,重要性排序如下(降序):
- 过渡声明 ------ CSS transitions
- 带
!important
的用户代理声明 ------ user-agent (browser) - 带
!important
的用户声明 ------ user - 带
!important
的网页作者声明 ------ author (developer) - 动画声明 ------ CSS @keyframe animations
- 普通的网页作者声明 ------ author (developer)
- 普通的用户声明 ------ user
- 普通的用户代理声明 ------ user-agent (browser)
源的重要性也可以理解为源的优先级,但是要区别于选择器的优先级。你现在只需要知道源的重要性如何排序(哪种源更重要),至于源的这个特点有何作用后面会提到。
只讨论开发者写的 CSS 代码排序为:过渡声明 > 带 !important
的网页作者声明 > 动画声明 > 普通的网页作者声明。
CSS 属性初始值
上面我们提到了用户代理样式表,<div>
元素经过用户代理样式表将 display
属性设置为了 block
,block
是浏览器样式表指定的值。如果给 <div>
元素设置 display:initial;
是什么效果?答案就是等价于设置为 diaplay: inline;
。
这是因为给一个元素某个属性的属性值设置为 initail
的效果为:将该属性恢复为 CSS 标准中的属性初始值;CSS 标准中定义 display
属性的初始值为 inline
。
所以不要将 CSS 属性初始值和元素该属性的默认值混淆,属性初始值来自 CSS 标准定义,元素在该属性的默认值是来自用户代理样式表。
CSS 关键字
initial
将属性的初始(或默认)值应用于元素。不应将初始值与浏览器样式表指定的值混淆。--------- MDN
大家可以自行查阅一些常见属性的初始值,比如 color
、font-size
等,看看是否知道这些属性的初始值。
层叠
CSS 的第一个字母 CSS 代表 Cascading ,就是层叠的意思。不知道你是否留意过在描述三大特性时,通常有两种顺序:
- 层叠、优先级、继承;
- 层叠、继承、优先级;
关于这个顺序后面还会有一些讲解,现在你肯定关注到了层叠始终是三大特性中第一个被描述的,结合 CSS 的第一个 C 就能够代表他的地位。MDN 上是这样描述层叠的:
层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。
CSS 层叠算法期望通过挑选 CSS 声明来给 CSS 属性设置正确的值。
层叠算法决定如何找出要应用到每个文档元素的每个属性上的值。
如果你对此概念不能理解,没有关系,我们先看看层叠的具体规则,搞清楚它要做什么,然后再来看定义就会轻松很多。
层叠的具体规则
之前我们提到过源的概念,已知浏览器都会有一个默认的样式表,给一些元素设置了默认样式。当我们去开发 CSS 的时候,也就可能导致一个元素的一个属性具有来自不同源的多次声明,这就是 CSS 规则冲突。层叠算法简单来说就是解决冲突的声明,找到最终要应用的唯一规则。层叠算法的具体规则如下:
第一步:过滤
层叠算法首先会收集所有元素所有属性 的声明值,每个属性都会输出一个无序列表。
这里有两种情况:
- 存在一到多个声明值:输出有值的无序列表(声明值来自开发者和浏览器的默认样式),进行下一步处理;
- 不存在声明值:输出空列表,层叠算法结束;
过滤是对于每个元素的每个属性而言的,都会输出一个列表(可能为空)。
在 w3c 中写到过滤是在层叠之前进行的,而在 MDN 中过滤则是层叠算法的第一步。本篇文章会将过滤作为层叠算法的第一步,这是我个人的理解。但是过滤是否为层叠算法的一部分,都不会影响层叠最终的结果,因此没必要为此纠结,你只需要知道该步骤做些什么输出了什么。
第二步:排序
当某个属性过滤输出一个有值的无序列表时,层叠算法会对该列表进行排序,排序的依据就是源的重要性 。层叠会计算出重要性最高的源来作为属性最终的规则,这里也有两种情况:
- 计算的结果是唯一的(只有一个属性值),那么该声明会直接应用;
- 计算的结果是不唯一,也就是有多个声明在重要性排序中胜出,这种情况下会进行下一步骤,优先级算法。
这也是我们写的 CSS 代码可以覆盖元素浏览器默认样式的原因,由于在预备知识已经提到过源的重要性排序,这里就不再赘述。
第三步:优先级算法
当我们给一个元素编写了多条规则(来自开发者而不是浏览器),这些规则在排序计算中都属于同一种源 ------ 用户样式表,这种情况下就需要优先级算法的判定。
优先级算法是指选择器的优先级,该算法会在排序胜出规则的基础上计算选择器的权重,权重最大的规则会被应用。
这里不会讲解选择器权重具体的计算方式,大家可以自行查阅,需要注意伪类伪元素的权重计算方式,以及一些特殊伪类的计算方式(:where
、:not
、:is
),这些在 mdn 上都有提到。
特别强调:
- 源的重要性排序始终先于优先级算法;
- 带上
!important
属于源重要性判定而不是优先级判定的;
同理当优先级算法无法选择出最终唯一结果时,需要进一步判定,依据源码顺序。
第四步:源码顺序
当存在多个优先级相同的声明时,后声明的规则生效。到这里,只要属性具有一到多个声明值,就一定可以计算出最终生效的声明值,解决冲突的规则,层叠算法就此结束。
另外重复一点:优先级算法始终先于源码顺序判定。
层叠的结果和层叠值
在了解了层叠的具有规则之后,回过头来看看它到底输出了什么或者说结果是什么。
不管属性是否具有声明值,层叠算法都会为每个属性输出属于该属性的无序列表,无序列表就是层叠的最终输出,有值的无序列表经过了排序等步骤,无值的是一个空列表。这也是我将过滤理解成层叠算法第一步的原因,因为过滤阶段就会输出还没有排序的无序列表。
The output of the cascade is a (potentially empty) sorted list of declared values for each property on each element.
--------- w3c
层叠算法为有声明值的属性计算出了最终生效的唯一属性值,这个属性值可以称为 层叠值 cascaded value ,没有声明值就没有层叠值。层叠值代表了层叠计算的结果,而空列表是无需计算的。
The cascade takes an unordered list of declared values for a given property on a given element, sorts them by their declaration's precedence as determined below, and outputs a single cascaded value.
The cascaded value represents the result of the cascade: it is the declared value that wins the cascade (is sorted first in the output of the cascade). If the output of the cascade is an empty list, there is no cascaded value.
------ w3c
疑问解答
这里有一些你可能存在的疑问,我给出个人的理解,仅供参考:
-
所有的属性都会经过层叠算法吗?没有声明值的属性会经过层叠算法吗?
我认为所有的属性都会经过层叠算法,标准也多次提到了层叠算法是对于每个元素的每个属性而言的。
-
层叠计算是如何理解的?
我理解的层叠算法可以分为两个阶段:收集声明、层叠计算。收集声明就是过滤阶段,过滤肯定会输出一个无序列表(空列表或者未排序的列表)。层叠计算理解为排序以及排序后的步骤,它们的效果就是计算出最终要生效的值或者说计算出层叠值,没有声明值的属性不需要进行层叠计算。
-
一个属性没有 CSS 声明有没有层叠值?
没有,一个属性只要有声明值就一定有唯一的层叠值,没有声明值就一定没有层叠值。层叠值代表层叠计算的结果。
-
层叠算法包含了优先级算法吗?
是的,mdn 上提到了层叠算法是先于优先级算法的,从步骤上来看也确实如此,但是它可能更想说的是源重要性排序先于优先级算法,这点很重要。
-
没有 CSS 声明的属性如何处理?
默认处理,后面会讲到
CSS 值的处理
前面我们知道了层叠算法对于有声明的属性计算出层叠值,那么没有声明值的属性应该用什么属性值?接下来的内容就是本篇的第二个重点,CSS 值的处理流程。
默认处理 Defaulting
所有的属性在经过层叠算法之后到了默认处理阶段,默认处理阶段每个属性会产生一个指定值 specified value,指定值从以下三个方式中获取:
- 如果有层叠值:指定值就是层叠值;
- 没有层叠值,该属性为可继承属性:指定值从父元素的该属性继承(层叠值如果为
inherit
也是该步骤); - 没有层叠值,该属性为不可继承属性:指定值为该属性的初始值(层叠值如果为
initial
也是该步骤);
经过默认处理,所有的属性一定会有一个属性值,这个时候的属性值就是指定值。
继承
默认处理阶段涉及到了继承,且该阶段发生在层叠算法之后,记得前面讲过三大特性的顺序吗,因此在描述三大特性的时候最好说:层叠、优先级、继承,在一些标准文档中也是这个顺序。
继承的一个特性就是父元素会将该属性的计算值 computed value 传给给子元素,计算值是通过指定值计算得到的。由于每个属性都存在一个指定值,因此也存在一个计算值,你可以认为它们是同时产生的。对于一个可继承属性来说,父元素的计算值会作为子元素的指定值。
计算值是布局之前的值 ,会尽可能的解析指定值,将相对值转换成绝对值:
- 对于已经是绝对值的指定值,不需要再进行其他计算,计算值就是指定值,比如
color:red;
、font-size:10px;
,它们已经是明确的值,不需要再近一步的计算; - 不是绝对值的指定值,需要进行计算,但是不一定能够得到一个明确的值;不管计算结果如何,计算值也都来自指定值;
举一些例子:
例子1:相对值可以转换为绝对值
html
<div class="box1" style="font-size: 20px">
<div class="box2" style="font-size: 2em;">
<p>
p 元素的字体大小继承父元素的字体大小
父元素字体大小的计算值为:20px * 2 = 40px
但是父元素的指定值是 2em
</p>
</div>
</div>
对于类名为 box1
、box2
的元素的 font-size
属性来说,20px
和 2em
分别是其对应的声明值、层叠值、指定值。对应 <p>
元素来说, font-size
属性没有声明值和层叠值,该属性可以继承,因此指定值为父元素 box2
同属性的计算值,box2
的计算值为 20px * 2 = 40px
。
box1
、<p>
元素的计算值则分别为 20px
、40px
,因为他们的指定值已经是一个明确的值,不需要进行其他的计算。<p>
元素字体大小的计算值 40px
又可能会作为其子元素同属性的指定值(因为可能设置了层叠)。
例子2:相对值无法转换为绝对值
html
<div class="box1">
box1 元素的宽度属性值是属性初始值 auto
<div class="box2" style="width: 50%;">
box2 元素的宽度需要根据 box1 的宽度确定
box1 元素的宽度在默认处理阶段无法确定,需要等到布局之后才能确定
</div>
</div>
从最终的效果上来看,大家肯定都能看出这几个元素的宽度会受到视口宽度的影响。
从原理上来看,由于此阶段还处于 CSS 的解析阶段,没有布局相关的信息。box1
的指定值为 auto
,box2
的声明值、层叠值、指定值为 50%
。由于 auto
、50%
在当前阶段都无法计算为一个明确的值,因此他们的计算值就是对应的指定值。
例子3:一个属性影响到另一个属性
css
span {
position: absolute;
}
该 <span>
元素的 display
属性会计算为 block
。
到这里,我们已经知道了声明值、层叠值、指定值、计算值是什么以及如何来的,这些流程都发生在布局之前。
计算布局
由于一些计算值还没有计算为绝对值,它们需要获取到布局相关的信息后,再次计算才能得到一个绝对的值,这种情况下产生的绝对值称为应用值 used value 。CSS 属性的应用值是指完成所有计算后最终使用的值 ,应用值是布局之后的值。如果一个属性的计算值已经明确了,应用值直接采用该计算值。
在上面的例子2中:站在开发者的角度上来看 box1
的宽度会随着视口的宽度变化,因此 box2
的宽度也会跟着变化,原理是布局之后将相对的计算值转换成了具体的像素值,auto
根据视口宽度变化,50%
根据父元素的宽度再次计算。
计算值与应用值的区别在于:计算值是布局之前的值,可能是明确的,也可能是相对的;应用值是布局之后的值。下面是 MDN 的描述:
最初,CSS2.0 定义的计算值 Computed values 就是属性的最终值。但是 CSS2.1 重新定义了 computed values 为布局前的值,used values 布局后的值。布局前与布局后的区别是,width 或者 height 的 百分比可以代表元素的宽度,在布局后会被像素值替换。
应用值虽然是最终的计算结果,但并不一定就是浏览器最终应用的实际值,实际值 actual value 是指应用值被应用后的近似值。比如设置一个盒子边框的宽度 为 1.9 px
,声明值、层叠值、计算值、指定值、应用值都是该值,但是最终的实际值其实是 1.5px
;设置为 2.49px
,最终的实际值是 2px
;这是因为浏览器无法为边框的宽度设置为 1.9px
或者 2.49px
(测试结果来自 edge)。
w3c 给出了一些例子,大家可以尝试计算:
总结
属性在经过层叠算法(包含优先级算法)、默认处理(包含继承、设置属性初始值)、计算布局产生最终浏览器的实际值,了解这些概念和流程帮助我们以可预测的方式开发 CSS。
回到最初的问题,关于 CSS 三大特性的理解,你现在已经知晓了其中的原理,我认为关键信息就是开头提到的主要内容,你应该有自己的理解,比起一个固定的模板答案,结合原理再加上自身经验并引入到自己擅长的领域或许是更好的回答。
那么本篇的主要内容就到此结束了,最后感谢大家的阅读,如有错误,请各位指正。
参考资料
MDN 中英版本: