CSS 中的样式计算

我想大家都深有体会,书写 CSS 是一件很痛苦的事情,几乎每个前端开发着都不想去写 CSS,所以为了方便我们去写 CSS 社区出现了各种各样的框架,比如用来 LESS 和 SASS,或者推崇 CSS in JS 的 Styled Component,或者基于原子化概念实现的 Tailwind CSS 和 UnoCSS。这些确实能简化我们的工作,但是不管以何种方式,最终到浏览器那边都还是最原始的样式表,所以了解 CSS 的基础知识对我们来说也十分重要。

在 CSS 里面有两个非常关键的知识

  • 样式计算 (Computing Style)
  • 视觉格式化模型(Visual Formatting Model)

学 CSS 这两个东西搞定了基本就差不多了,其他的都是些边角料了。别看那些 CSS3 的属性一大堆,都是边角料。但是如果这两块没有掌握的话,那就相当于 CSS 没有掌握,不管你写了多少年的 CSS 就是没有掌握。我们这次主要把样式计算一起过一遍,这里面的东西很多,大家也可以做一个自我检查,看一下这里面自己还有哪些东西没有搞清楚。

CSS 样式计算又称 CSS 属性值的计算过程。什么叫 CSS 属性值的计算过程?它指的就是任何一个 HTML 元素,它从所有的 CSS 属性都没有值,到它所有的 CSS 属性都有值,中间需要有一个计算过程。而这个计算过程就叫作 CSS 属性值的计算过程。有的同学可能有以为,哪些 CSS 属性有值不是我们自己写的吗?只要我写了就会有值,没写的就不会有。其实不是这样的,我们随便找一个元素,打开控制台,切换到 Computed 标签下面,勾选 show all,这些就是它所有的计算属性,你说哪一个没有值?都有。浏览器必须要保证所有的 CSS 属性全部都有值,它才能显示。就是 W3C 上官网上有多少个 CSS 属性,那么这个元素就必须要保证每个属性都有值。而我们这次要研究的,就是浏览器是如何来确定每一个属性值的。浏览器确定每个 CSS 属性值的过程,就称为 CSS 属性值的计算过程。整个计算过程共分成了四个步骤:

  1. 确定声明值
  2. 层叠
  3. 继承
  4. 默认

我们通过下面这个例子,来看一下 CSS 属性值计算的这四步都在做什么。 https://stackblitz.com/edit/typescript-uvpmb2?file=index.html,index.ts,style.css

样式
color?
text-align
font-size
background-color
font-weight
display
其他

确定声明值

有一个 h1 元素,浏览器为了确定它的每一个 CSS 的值,它会首先看样式表,样式表一共有两个:

  • 作者样式表

  • 浏览器默认样式表

作者样式表很容易理解,就是这个页面的作者,也就是我们写的样式表,另外一个就是浏览器的默认样式表,浏览器是有默认样式的。比如这个 h1 元素,浏览器会给它一些默认样式。

demo.png

顺便说一句,大家看这个 h1 里的默认样式里面写了一个 display: block 因此它会独占一行。并不是因为 h1 是一个块级元素,能不能独占一行看的不是 HTML 是什么,而是看 CSS 是什么,我们应该这样说:h1 元素在默认情况下生成块盒。这才是最准确的说法,不过这是说法的问题,大家懂什么意思就行,不过大家以后在描述东西的时候,也应该尽量描述准确。

不管是作者样式表还是浏览器默认样式表,这里面写的东西都叫作声明值,在这些样式里面,浏览器要先去找哪些样式是没有冲突的,没有冲突的意思是针对这个 h1 元素它应用的样式没有重复的,这个例子中没有冲突的有哪些?有 color display font-weight 找到这些没有冲突的,直接生成的它的结果样式表里面。顺便说一句这边的计算还会做一件事情就是把那些相对单位换算成绝对单位,比如字体大小的 2em,把预设值换算成绝对值,比如 bold 换算成 700,red 换算成 RGB 值。

样式
colorrgb(255, 0, 0)
font-size
background-color
font-weight700
displayblock
text-align?
其他

总结一下这就是第一步,把样式表里面没有冲突的确定下来。

下面是第二步:层叠

层叠

层叠是 CSS 中非常重要的概念,这个概念有多重要呢?CSS 就叫作层叠样式表(Cascading Style Sheets)。层叠只做了一件事,那就是解决冲突。我们在第一步中已经搞定了哪些没有冲突的样式。那些有冲突的怎么办?

有些同学不知道在哪里看到文章会说到有权重,会说 ID 选择器大于类选择器,类选择器大于元素选择器,通过这样记是非常肤浅的。有好多复杂的场景是很难通过这个规则讲清楚,而且 CSS 的官方文档中也从来没有说过权重这个词,而是通过下面这三步来解决冲突。

  1. 比较重要性
  2. 比较特定性
  3. 比较源次序

解决冲突的其实就是一个吃鸡游戏,上面三步就是按照顺序一层一层的筛选,最终只剩下一个其他的全部淘汰。

比较重要性

作者样式表 !important > 默认样式表 !important > 作者样式表 > 默认样式表

CleanShot 2023-10-23 at 19.03.36@2x.png

比如 这个 例子中,按照我们之前的理解 * 应该是权重最低的,但是为什么样式会覆盖掉元素选择器呢?因为下面这个是浏览器的默认样式表。所以这个时候根本就还没有去看各种选择器权重之类的,第一步就是去看重要性,如果第一步通过重要性就已经比较出来了,就没必要再执行后面的过程。

所以一旦我们在作者样式表里面加了 !important,它的重要性就会变成最高。这也就是我们尽量不要使用 !important 的原因,因为一旦加了这个别人就很难覆盖掉它,除非再加一个 !important,那么到最后整个工程就会充斥者各种各样的 !important 导致样式表难以维护。

比较特定性

在上一步重要性比较完成之后,如果还有冲突,会进入特定性(Specificity)的比较。特定性算出来的结果会是 4 个数字,比较的方式是从第一位开始比较取大的那一个,如果相同则比较下一位。

  • 第一位: 内联:1 / 否则:0
  • 第二位: ID 选择器的数量
  • 第三位: 类选择器,伪类选择器,属性选择器的数量
  • 第四位: 元素选择器,伪元素选择器的数量。

通过特定性的比较,上述的例子我们又可以确定几个值:

样式
colorrgb(255, 0, 0)
font-size
background-colorrgb(0, 128, 0)
font-weight700
displayblock
text-align?
其他

比较源次序

如果比较完特定性之后还没有解决冲突,就会比较源次序,源次序的意思是在源代码里面的书写顺序,后书写的样式覆盖前面的样式。有些同学在这边会有些误解,他会认为在使用 class 的时候和 class 值的顺序有关,其实是没有关系的,class=“a b”class=”b a” 效果是一样的。

所有才有了 CSS 中超链接样式的爱恨原则(LoVe HAte)

a {/* 处于任意状态的a标签,不论是超链接还是锚点 */}
a:link {/* 未访问过的超链接 */}
a:visited {/* 访问过的超链接,点击超链接再返回当前页,这个超链接就处于visited状态 */}
a:focus {/* 获得焦点的超链接,tab键选中超链接或者长按超链接再移开鼠标 */}
a:hover {/* 鼠标悬停的超链接,鼠标经过超链接时或悬停在超链接上时,这个超链接就处于hover状态 */}
a:active {/* 处于激活状态的超链接,鼠标在超链接上按下时 */}

正是也被上面的选择器特殊性是一样的,所以为了保证样式的正确,我们必须安装这个顺序书写。

最后经过源次序的比较,我们又可以确定一些 CSS 属性值

样式
colorrgb(255, 0, 0)
font-size30px
background-colorrgb(0, 128, 0)
font-weight700
displayblock
text-align?
其他

上面两步执行完成之后,会发现还有些 CSS 属性是没有值的,我们要保证所有的属性都是有值的才行,所以进入了第三步继承。

继承

对仍然没有值的属性,如果可以继承,则使用继承。

因为不是所有的属性都可以继承的,准确地说是不是所有的属性都可以自动继承的。

在 CSS 中,每个CSS 属性定义的概述都指出了这个属性是默认继承的 ("Inherited: Yes") 还是默认不继承的 ("Inherited: no")。这决定了当你没有为元素的属性指定值时该如何计算值。

继承属性

当元素的一个继承属性(inherited property)没有指定值时,则取父元素的同属性的计算值(computed value)。只有文档根元素取该属性的概述中给定的初始值(initial value)。

非继承属性

当元素的一个非继承属性(在 Mozilla code 里有时称之为reset property ) 没有指定值时,则取属性的初始值 initial value(该值在该属性的概述里被指定)。

注意

inherit 关键字允许显式的声明继承性,它对继承和非继承属性都生效。

可以使用 all 简写属性一次控制所有属性的继承,该属性将其值应用于所有属性,例如:

font {
  all: revert;
  font-size: 200%;
  font-weight: bold;
}

至此我们又可以确定一些 CSS 属性值

样式
colorrgb(255, 0, 0)
font-size30px
background-colorrgb(0, 128, 0)
font-weight700
displayblock
text-aligncenter
其他

默认

那些无法自动继承的属性,如果还没有值,则会使用默认值,我们可以在 CSS 官方文档中查询到所有属性的默认值(initial value)。 最终,浏览器会把所有的 CSS 属性都确定下来,这就是样式计算的过程。