图解CSS:CSS层叠和继承

发布于 大漠

CSS中有三个概念是学习CSS必须要掌握的:层叠继承权重。今天我们主要来了解CSS中的层叠和继承,对于CSS权重这一部分将放到CSS的选择器中来介绍,因为这一部分和CSS的选择器耦合的更为紧密。不管是初学者还是有一定工作经验的同学,花点时间阅读这篇文章都是很有必要的,这样有利于你对CSS更清楚的了解和理解。感兴趣的同学请继续往下阅读。

在很多Web开发人员眼中,CSS不是一门程序语言,但它真真切切的是一门计算机语言。主要用来为结构化文档,比如HTML、XML等添加样式,其主要由W3C定义和维护。而CSS是由Cascading Style Sheets三个词的首字母缩写,很多人将其称为层叠样式表或者级联样式表。而我们今天要讲的第一个概念就是CSS中的层叠,也对应的是CSS中的第一个字母C。看到这里,或许你就知道为什么会说层叠是CSS的重要概念之一了。

层叠

层叠是CSS固有的一个东东,它赋予了层叠样式表层叠性。层叠可能是一个很强大的工具,但如果错误的使用它可能会导致样式表的脆弱性,使用Web开发人员在任何时候不得不进行更改时都感到头痛。比如说:

初学CSS的同学,特别是Web其他端的程序员(比如服务端、客户端)在独立编写CSS的时候,经常会碰到这样的一个现象:为什么写的样式不起作用呢?面对这样的场景很多人会采用非常暴力的手段来处理,比如通过添加!important或直接在HTML的元素上添加内联CSS。这就是令很多同学感到疑惑的一点?为什么要这样做呢?其实这和接下来要介绍的层叠(也有很多人称之为级联)有很大的关系。

层叠的定义

因为我们将要深入的学习和讨论CSS层叠是如何工作的相关细节,因此我们有必要的了解W3C规范是如何定义它的:

The cascade takes a unordered list of declared values for a given property on a given element, sorts them by their declaration’s precedence, and outputs a single cascaded value. —— W3C规范

大致意思是:该层叠将获取给定元素上给定属性的声明值的无序列表,按声明的优先级对它们进行排序,并输出单个层叠值

CSS层叠是一种算法,浏览器通过它来决定将哪些CSS样式规则应用到一个元素上。很多人喜欢把它看作是“获胜”的样式,按照CSS中的术语来说,它的权重更高(后面我们会深入介绍)。

为了更好的理解CSS层叠,将CSS声明看作具体的”属性“(attributes)。这些属性可以是声明的各个部分比如说CSS选择器或CSS属性,甚至是CSS声明的位置相关(比如它的原始或源代码中的位置)。

CSS层叠将会根据自己的算法,采用这些属性中的一些,并为每个属性分配一个权重。如果CSS规则在高优先级上获胜(选择器权重高),那么这个样式规则就会获胜,即生效。但是,如果在给定的权重下极然有两个规则冲突,算法将继续”向下层叠”,并且会检查低优先级属性,直到找到一个胜出的规则。

简单的说,当多个相互冲突的CSS声明应用于同一个元素时,CSS层叠算法会根据一定的机制,从最高权重到最低权重的顺序列出:

  • 来源和重要性
  • 选择器权重
  • 出现的顺序
  • 初始和继承属性(默认值)

接下来围绕这几点来进行展开。

来源和重要性

层叠检查的最高加权属性是给定规则的重要性和来源的组合。就CSS规则的来源而言,规则主要来自三个地方:

  • 编写者规则(Author):这是HTML文档声明的CSS。也就是我们前端开发人员编写的,根据文档语言(比如HTML)约定给源文档指定样式表。这也是我们能够控制的唯一来源
  • 用户(User):这是由浏览器的用户定义和控制的。不是每个人都会有一个,但是当人们添加一个时,通常是为了覆盖样式和增加网站的可访问性。比如,用户可以指定一个售有样式表的文件,或者用户代理可能会提供一个用来生成用户样式(或者表现得像这样做了一样)的界面
  • 用户代理(User-Agent):这些是浏览器为元素提供的默认样式。这就是为什么input在不同的浏览器上看起来略有不同,这也是人们喜欢使用CSS重置样式,以确保重写用户代理样式的原因之一

注意,用户可能会修改系统设置(例如,系统配色),这会影响默认样式表。然而,有些用户代理实现让默认样式表中的值不可改变

这三种样式表将在一定范围内重叠,并且它们按照层叠互相影响。

CSS层叠给每个样式规则赋予了权重,应用几条规则时,权重最大的优先。默认情况下,编写者样式表中的规则比用户样式表中的规则权重高。CSS声明的重要性由适当命名的!important语法决定。!important的CSS规则自动将它跳到层叠算法的前面,这也是为什么不鼓励在样式中使用!important的原因之一。覆盖使用!important的样式只能使用其他的!important的规则来完成,如果你的项目足够大,用的!important又足够地多,那么你的CSS就会变得更为脆弱,更难于维护。对于!important的使用,建议你只在其他所有方法都失效的情况之下使用。

那么CSS中层叠算法又是如何判断哪个声明获胜:

CSS层叠在判断哪个声明获胜时考虑这两个属性的组合。每个组合都有一个权重(类似声明的部分权重),权重最高的规则获胜。以下是浏览器考虑的各种来源和重要性的组合,按从最高权重到最低权重的顺序列出:

  • !important的用户代理规则
  • !important的用户规则
  • !important的编写者规则
  • CSS动画@keyframes中的规则(这是较为特殊的一部分,它仍然来自编写者规则,但是由于动画是临时的或短暂的,所以浏览器对它们的权重略高于正常的编写者规则)
  • 编写者规则
  • 用户规则
  • 用户代理规则

当浏览器遇到两个或更多冲突的CSS声明,其中一个在来源和重要性级别获胜时,CSS层叠就会解决这个规则。但是,如果相互冲突的声明具有相同的重要性和来源级别,则层叠将继续考虑选择器的权重。

选择器权重

CSS层叠中的第二个权重是选择器的权重。在这个层中,浏览器查看CSS声明中使用的选择器。作为前端开发人员,我们只能控制编写者样式规则。因为我们无法对来源的规则做太多的更改。但是,你会发现,只要在代码中不使用!important,对于CSS层叠中的选择器权重这一层,还是有较多的方式可以控制。

对于一个选择器的权重,将会按下面这样的规则进行计算:

  • 如果声明来自一个行内样式(style属性)而不是一条选择器样式规则,算1,否则就是0=a)。HTML中,一个元素的style属性值是样式规则,这些属性没有选择器,所以a=1, b = 0, c = 0, d = 0,即**1, 0, 0, 0**
  • 计算选择器中ID属性的数量 (= b)
  • 计算选择器中其它属性和伪类的数量 (= c)
  • 计算选择器中元素名和伪元素的数量 (= d)

4个数连起来a-b-c-d(在一个基数很大的数字系统中)表示特殊性,比如下面这样的示例:

*             {}  /* a=0 b=0 c=0 d=0 -> 选择器权重 = 0,0,0,0 */
li            {}  /* a=0 b=0 c=0 d=1 -> 选择器权重 = 0,0,0,1 */
li:first-line {}  /* a=0 b=0 c=0 d=2 -> 选择器权重 = 0,0,0,2 */
ul li         {}  /* a=0 b=0 c=0 d=2 -> 选择器权重 = 0,0,0,2 */
ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> 选择器权重 = 0,0,0,3 */
h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> 选择器权重 = 0,0,1,1 */
ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> 选择器权重 = 0,0,1,3 */
li.red.level  {}  /* a=0 b=0 c=2 d=1 -> 选择器权重 = 0,0,2,1 */
#x34y         {}  /* a=0 b=1 c=0 d=0 -> 选择器权重 = 0,1,0,0 */
style=""          /* a=1 b=0 c=0 d=0 -> 选择器权重 = 1,0,0,0 */

在互联网上也有很多很形象的图来解释CSS选择器权重的,比如下图:

上图来自于@Estelle Weyl的《CSS Specificity》一文

有关于CSS选择器权重更详细的介绍,我们将在CSS选择器一文中进行阐述,如果你想先进行了解,可以阅读@Vitaly Friedman的《CSS Specificity: Things You Should Know》一文。译文可以点击这里。也可借助@Keegan Street选择器权重计算工具更形象的了解CSS的选择器权重的计算。

出现的顺序

CSS层叠算法的最后一个主要层是按源码中出现的顺序来计算。当两个选择器具有相同的权重时,在源代码中最后的声明获胜。

因为CSS在层叠中考虑源顺序,所以加载样式表的顺序就显得尤为重要。如果在HTML文档的head引入了两个样式表,那么第二个样式表将覆盖第一个样式表中的规则。

初始和继承属性

虽然初始值initial和继承值inherit并不是CSS层叠中真正组成部分,但是如果没有针对元素的CSS声明,它们将确定发生什么。通过这种方式,它们确定元素的默认样式值。

有关于初始和继承更详细的介绍,将在下一节中进行详细地阐述。感兴趣的同学,请继续往下阅读。

参与层叠计算的CSS实体

只有CSS声明,就是属性名值对,会参与层叠计算。这表示包含CSS声明以外实体的@规则不参与层叠计算,比如包含描述符的@font-face。对于这些情形,@规则是做为一个整体参与层叠计算,比如@font-face的层叠是由其描述符font-family确定的。如果对同一个描述符定义了多次 @font-face,则最适合的 @font-face是做为一个整体而被考虑的。

包含在大多数@规则的CSS声明是参与层叠计算的,比如包含于@media@documents或者@supports的CSS声明,但是包含于@keyframes的声明不参与计算,正如@font-face,它是作为一个整体参与层叠算法的筛选。

注意 @import@charset遵循特定的算法,并且不受层叠算法影响

继承

在CSS中,每个CSS属性定义的概述都指出了这个属性是默认继承的还是默认不继承的。这决定了当你没有为元素的属性指定值时该如何计算值。当元素的一个 继承属性没有指定值时,则取父元素的同属性的计算值。只有文档根元素取该属性的概述中给定的初始值;当元素的一个非继承属性没有指定值时,则取属性的初始值。比如:

html {
    font-size: small;
}

这个规则在HTML文档的根元素html设置了一个font-size属性,而这个属性是会被继承的(在CSS中有些属性天性就会被继承)。正如上面所示,html元素的所有找后元素都将被继承这个属性,比如下衅中蓝框中显示的一样:

上图蓝框中告诉body元素继承html元素中的font-size: small;。开发者工具中会提示我们“Inherited from html”。那么问题来了,在CSS中哪些属性是会被继承的?其实在W3C规范中各个属性的描述已经很清楚的告诉我们了。比如说border属性,在描述其语法时,在列表中有一个Inherited描述项的值为no。这也就告诉我们border属性是不能被继承的。反之,再看font-size属性,语法描述的列表中同样有一个Inherited描述项,只不过它的值不是no,而是yes,也就是说font-size属性是会被继承的。

这决定了当你没有为元素的属性指定值时该如何计算值。

如果你平时阅读规范仔细的话,不难发现,在介绍每个属性的语法参数的时候,都会有一个Initial参数,该参数主要指定每个属性的初始值。CSS属性已经给出的初始值针对不同的继承和非继承属性有不同的含义:

  • 对于继承属性,初始值只能被用于没有指定值的根元素上
  • 对于非继承属性,初始值可以被用于任意没有指定的元素上

这样我们引出两个概念:初始值继承值,除了这两个概念之外,在CSS属性中还有一个计算值,该值由指定的值计算而来:

  • 处理特殊的值inheritinitial
  • 根据属性的摘要中关于计算值描述的方法计算出值

计算属性的计算值通常包括将相对值转换成绝对值,比如em%这样的单位。例如:

font-size: 16px;
padding-top: 2em;

其中padding-top: 2em就是一个计算值,其计算出来的值将根据font-size做为基数计算(在此示例中),在此计算出来的值是32px。然而,有些属性的百分比值会转换成百分比的计算值(比如width)。另外,line-height属性值是没有单位的数字,其值也是一个计算值。

对于CSS的计算值,在不同的浏览器中其计算出来的值有时候会略有偏差。

如果你感兴趣的话,可以打开浏览器的开发者工具,查看对应的计算值(比如,Chrome开发者工具,有一个Computed选项,该选项展示的就是对应的CSS计算值),如下图所示:

其中计算值的最主要用处是继承,包括inherit关键词。

最后总结两点:

  • 当元素的一个继承属性没有指定值时,则取父元素的同属性的计算值,只有文档根元素取该属性的概术中给定的初始值
  • 当元素的一个非继承属性没有指定值时,则取属性的初始值

看到这里,或许你知道了什么叫继承和非继承,以及他们取值方式。但你可能还在纠结,在CSS中到底哪些属性是继承属性,哪些不是继承属性?其实这个问题我也没办法准确的回答您,因为我也没有做过这方面的统计。不过我可以告诉大家两个小经验:

  • 在CSS中一些关于字体、文本和颜色等属性都是可继承属性
  • 在CSS中一些跟布局和盒子模型的属性都是非继承属性

如果你想准确的知道答案,可以通过这里整理的属性表格进行统计。只要Inherited选项是Yes的都表示是继承属性,否则都是非继承属性。

处理CSS继承的机制

在CSS中提供了处理CSS继承的机制,简单地讲就是CSS提供了几个属性值,可以用来处理属性的继承。这几个属性值就是initialinheritunsetrevert。其实除了这四个属性值之外,还有一个all属性值。虽然这几个属性值主要用来帮助大家处理CSS属性继承的,但他们之间的使用,还是有一定的差异化。接下来我们一看看这几个属性值的实际使用以及对应的差异化。

initial

在CSS中,每个属性都具有一个初始值,其实也就是CSS属性的默认值。在CSS规范中,都对每个属性的初始值做出了相关的定义。比如text-align的初始值是leftdisplay的初始值是inline

而这里,我们要说的是CSS的关键词initial

If the cascaded value is the initial keyword, the property’s initial value becomes its specified value.

大致的意思是:“如果层叠值是initial关键词,则属性的初始值将成为其指定值”。换句话说,如果你在元素样式的设置中显式的设置某个属性的值为initial时,其实就表示设置了该属性的默认值。

从文字上理解可能有点困惑,我们通过一个小例来帮助大家理解。假设我们有一个<p>元素,接触过CSS的同学都知道,它是一个块元素,为了好看,咱们添加一点修饰的样式代码:

p { 
    background: #f36; 
    padding: 2rem; 
    font-size: 2rem; 
    color: #fff; 
}

效果看起来像下面这样:

如果我们希望p元素变成行内元素时,按照我们以前的处理方式,需要手动处理浏览器默认样式(User-Agent 用户代理样式),也就是显示的重置:

p{
    dispaly: inline;
}

blockinline效果对比如下:

前面提到过inlinedisplay的初始值(也就是默认值),而在规范中也提到过:

你在元素样式的设置中显示的设置某个属性的值为initial时,其实就表示设置了该属性的默认值

也就是说,我们可以给display设置initial关键词:

p {
    display: initial;
}

这个时候得到的效果其实和使用display:inline是一样的:

如果我们通过浏览器检查器中的计算值(Computed)一项可以看出来,display设置为initial时,会覆盖用户代理的样式值block

接下来,我们再来看一个继承属性color。所以<p>元素的后代元素<strong>也会继承<p>元素中设置的color: #fff值。如果我们显式的在strong中设置color的值为initial时,那么strongcolor将重置为默认值。由于我们没有设置默认的color颜色,那么这个时候,浏览器将会把一个计算值赋予成color的初始值:

inherit

CSS添加了一个inherit关键词属性值,可以让元素强制继承父元素的某个属性的值。前面也说过,CSS中有些属性自动就是可继承属性,比如font-sizecolor之类,但也有很多属性又是非继承属性,比如borderborder-radius之类的。在这里,如果在非常继承的属性上显示的设置了inherit关键词,表示该元素将继承父元素指定的属性值或者计算值。

为了同样的能更好理解inherit,来看一个示例,在这个示你中,我们用border来做例子:

<div class="wrapper" style="border:5px solid blue;"> 
    <div>...</div>
</div>

从效果上可以看出来,div.wrapper上的border样式并没有继承其子元素div上。如果我们想让.wrapper的子元素div也要具有与.wrapper元素的border样式,就需要在该元素div上显式设置相同的border样式:

<div class="wrapper" style="border:5px solid blue;"> 
    <div style="border:5px solid blue;">...</div>
</div>

有了inherit关键词之后,事情要变得简单地多,只需要在.wrapper的子元素上设置border: inherit;

<div class="wrapper" style="border:5px solid blue;"> 
    <div style="border: inherit;">...</div> 
</div>

得到的效果将是一样的:

上面的示例是父元素设置了border样式,所以其继承了父元素的border样式。那如果将上面的示例稍做修改,在元素外套一个div,而这个div不做任何样式的设置。将又会变成一个什么样呢?直接上示例吧:

<div class="wrapper" style="border:5px solid blue;"> 
    <div> 
        <div style="border: inherit;" class="ele">...</div> 
    </div> 
</div>

效果如下:

可以看到div.ele仅继承了其父元素divborder属性的计算值,并未继承其祖先元素.wrapperborder属性的设置值,通过浏览器开发者工具,可以看得一目了然:

这个示例说明:仅管元素自身显式的设置了inherit关键词,但是,如果其父元素没有明确指定样式,那么其最终效果将和revert的效果一致。即继承的是其父元素的计算值,也就是浏览器默认样式(User Agent Stylesheet)

revert

revert值早前被称之为default。表示没有使用任何属性值。

我们都知道,如果没有使用作者样式表(也就是Web开发人员自己写的样式表),那么浏览器将会按这样的过程去检测,元素调用的样式:

  • 浏览器首先会检测元素是否有使用作者样式表的属性
  • 如果没有找到,将会检测客户端默认样式
  • 如果没有找到客户端样式,相当于元素应用了unset

还是拿示例来说吧。比如我们一个div元素,我们并没有显式的在自己的样式表中设置其display属性的值。对于最后的渲染结果,浏览器将会使用用户代理规则的样式display:block

根据前面介绍的,就算是我们在div中显式的设置display:revert,该元素也将使用用户代理规则中的display:block样式。同样,我们在另一个div元素中使用display:initial,根据前面介绍的,那在这个div将会采用display的初始值inline。比如下面的效果:

我们再来看一个继承属性的运用场景。因为在div元素上设置了color:#fff;样式,这是用户写的样式,而且color是一个继承属性,只要是div的后代元素都将会继承color的属性值。根据前面所说revert的检测机制是,先检测用户自己写的样式,然后再检测用户代理样式,如果两都没有,才会设置unset的样式。所以最终我们看到的效果如下:(第二个设置了color:initial;)。

unset

unsetinitialinherit的组合。当属性设置为unset时,如果它是一个继承属性,那么它相当于是inherit;如果它不是,则相当于initial

有一些属性,如果没有明确指定,将默认是inherit。比如,我们给元素设置一个color,那它将适用于所有默认的子元素。而其它属性,如border则默认是非继承属性。

<div class="wrapper" style="border: 5px solid blue;color: #fff;">
    <div class="ele">...</div>
</div>

此时效果如下:

示例中color属性被继承了,但border属性没有被继承。

将上面的示例代码稍作调整:

<div class="wrapper" style="border: 5px solid blue;color: #fff;">
    <div class="ele" style="border: unset; color:unset;">...</div>
</div>

div.ele元素的bordercolor都设置了unset值。也就是说它将运用initial或者inherit的值。具体取决于哪个值,这得根据属性的默认行为是什么来决定。如果默认属性是inherit,那将运用的是inherit;如果默认属性是initial,那将运用的是initial

上面的示例中,border属性采用的是initialcolor属性采用的是inherit

all

在CSS中,all是一个简写属性,其重设除了unicode-bididirection之外的所有属性至它们的初始值继承值all有三个值:

  • initial:该关键字代表改变该元素或其父元素的所有属性至初始值。
  • inherit:该关键字代表改变该元素或其父元素的所有属性的值至他们的父元素属性的值。
  • unset:该关键字代表如果该元素的属性的值是可继承的,则改变该元素或该元素的父元素的所有属性的值为他们父元素的属性值,反之则改变为初始值。

来看示例。比如我们一个这样的一个HTML结构:

<div>...<strong>...</strong> ...</div>

给他们设置一些样式:

body { 
    padding: 2vw; 
} 
div { 
    background: #f36; 
    padding: 2rem; 
    font-size: 2rem; 
    color: #fff; 
    margin-bottom: 3rem; 
} 
strong { 
    font-size: 3rem; 
}

看到的效果如下:

这效果正是我们想要的。div显式的指定了backgroundpaddingfont-sizecolormargin-bottom属性值,而其中backgroundpaddingmargin-bottom非继承属性,而colorfont-size继承属性。除此之外,div还有一个客户端代理样式display:block,这个属性也是一个非继承属性。另外strong元素设置了一继承属性font-size,这个元素默认情况下继承了共父元素的color属性,同时还继承了html元素的font-familyline-height属性。当然,strong元素也有一个客户端代理样式font-weight:bold

上面看到的效果是我们平时使用的时候效果。如果这个时候,我们在divstrong同时设置all:inherit;时,得到的效果和前面的效果完全不一样:

这个时候divstrong重置了当初自己设置的属性,并且继承了各自父元素的一些属性:

  • div元素继承了body元素的paddingfont-familyline-height,同时也继承了body代理客户端的样式colorbackgrounddisplay
  • strong元素继承了div元素的样式

所以最后你看到的效果就像上图那样子。我们再把all的值设置为initial

这个时候divstrong样式都重置到了对应的初始样式,也就是对应的属性的默认样式,包括代理客户端样式也重置为对应属性的初始值。

最后来看all的值设置为unset的效果,下面的示例,我只在strong元素上设置all:unset,其效果就足以说明一切:

效果中的第一个是没设置all:unset,第二个设置了all:unset。这个时候strong元素的font-sizefont-weight继承了其父元素的font-sizefont-weight

all在CSS中有时候是一个属性,比如这里说的就是属性,但有的时候它还是CSS中某些属性的值。比如我们常在transition中用到的all,那这个时候就是属性值。到目前为止,CSS中的all属性也得到了众多浏览器的支持。

CSS样式的计算

CSS属性的最终值会经过四步计算:

  • 通过指定来确定值,常称之为指定值(Specified Value)
  • 接着处理得到一个用于继承的值,常称之为计算值(Computed Value)
  • 然后如果有必要的话转化为一个绝对值,常称之为应用值(Used Value)
  • 最后根据本地环境限tmhj进行转换,常称之为实际值(Actual Value)

那么什么是指定值计算值应用值实际值呢?

指定值

用户代理必须先根据下列机制(按优先顺序)给每个属性赋值一个指定值:

  • 如果层叠产生了一个值,就使用它
  • 否则,如果属性是继承的并且该元素不是文档树的根元素,使用其父元素的计算值
  • 否则使用属性的初始值,每个属性的初始值都在属性定义中指出了

计算值

指定值通过层叠被处理为计算值,例如,URI被转换成绝对的,emex单位被计算为像素或者绝对长度。计算一个值并不需要用户代理渲染文档。用户代理规则无法处理为绝对URI的话,该URI的计算值就是指定值。

一个属性的计算值由属性定义中Computed Value行决定。当指定值为inherit时,计算值的定义可以依据继承中介绍的规则来计算。即使属性不适用(于当前元素),其计算值也存在,定义在'Applies To'行。然而,有些属性可能根据属性是否适用于该元素来定义元素属性的计算值

应用值

处理计算值时,尽可能不要格式化文档。然而,有些值只能在文档布局完成时确定。例如,如果一个元素的宽度是其包含块的特定百分比,在包含块的宽度确定之前无法确定这个宽度。应用值是把计算值剩余的依赖(值)都处理成绝对值后的(计算)结果。

实际值

原则上,应用值应该用于渲染,但用户代理可能无法在给定的环境中利用该值。例如,用户代理或许只能用整数像素宽度渲染边框,因此不得不对宽度的计算值做近似处理,或者用户代理可能被迫只能用黑白色调而不是全彩色。实际值是经过近似处理后的应用值。

小结

本文涉及了大量的内容,希望它能帮助大家理解样式是如何受到我们编写和应用的影响。特别是CSS的层叠和继承。在实际使用的时候,如果很好的运用这些概念和手段,可以更好的帮助大家少写很多样式代码,而且更易于维护自己的CSS代码。

用张图来表示如下:

The 13th Version UA Yeezy Boost 350 Turtle Dove, the perfect version