有趣的CSS优先级

1,377 阅读6分钟

前言

无论在平时的开发中还是面试中,CSS优先级都是前端开发绕不开的问题,网上的资料也很多。但是,有些资料不够严谨。作为一名程序员,必须有点求真的精神嘛。于是,我决定拨开这层迷雾。

style和选择器

CSS2标准里这样描述:

A selector's specificity is calculated as follows:

  • count 1 if the declaration is from is a 'style' attribute rather than a rule with a selector, 0 otherwise (= a) (In HTML, values of an element's "style" attribute are style sheet rules. These rules have no selectors, so a=1, b=0, c=0, and d=0.)
  • count the number of ID attributes in the selector (= b)
  • count the number of other attributes and pseudo-classes in the selector (= c)
  • count the number of element names and pseudo-elements in the selector (= d) The specificity is based only on the form of the selector. In particular, a selector of the form "[id=p33]" is counted as an attribute selector (a=0, b=0, c=1, d=0), even if the id attribute is defined as an "ID" in the source document's DTD.
    Concatenating the four numbers a-b-c-d (in a number system with a large base) gives the specificity.

再结合CSS3标准关于选择器的描述:

A selector's specificity is calculated as follows:

  • count the number of ID selectors in the selector (= a)
  • count the number of class selectors, attributes selectors, and pseudo-classes in the selector (= b)
  • count the number of type selectors and pseudo-elements in the selector (= c)
  • ignore the universal selector Concatenating the three numbers a-b-c (in a number system with a large base) gives the specificity.

可以简单归纳为:

  • 如果样式定义在style里,给a赋1,记为a=1
  • 如果样式定义在id选择器里,统计id选择器的数量,记为b
  • 如果样式定义在class选择器、属性选择器、伪类选择器里,统计选择器数量,记为c
  • 如果样式定义在元素类型选择器、伪元素选择器里,统计选择器数量,记为d
  • 忽略通配符选择器

然后通过a、b、c和d的值可以算出样式的特异性(值越大,优先级越高)。

:is():not():has()这些伪类的特异性由它包含的选择器决定,:where()的特异性为0

特异性的计算规则有点像数字的进位,举几个例子:

选择器特异性简单表示
stylea=1,b=0,c=0,d=01,0,0,0
#btna=0,b=1,c=0,d=00,1,0,0
li.red.levela=0,b=0,c=2,d=10,0,2,1
div[data-index=0]a=0,b=0,c=1,d=10,0,1,1
ul ol+lia=0,b=0,c=0,d=30,0,0,3
*a=0,b=0,c=0,d=00,0,0,0

就像数字一样,高位数字比低位数字权重高。所以,如果上面按10进制计算的话,style的特异性为1000, #btn的特异性为100,li.red.level为21,*则为0。 ​

但实际上标准并没有规定按多少进制来计算特异性,只丢出了一句话,让厂商们自己体会。 image.png

in a number system with a large base

意思是说以一个比较大的数为基就行了。 ​

网上有这样的说法:通配符选择器优先级为0,元素类型选择器为1,class选择器为10,id选择器为1000,style更高,!important无穷大。我认为这是不严谨的,因为实际上10个class选择器累加起来的特异性并不等于1个id选择器。 ​

那么"a large base"到底有多大呢?不同的厂商有不同的实现。可能是出于性能的考虑(存储位数越小,性能越好),早期的Webkit和Mozilla都以8位二进制计算,也就是以256(2^8)为基,而Opera是以16位二进制计算的(65536=2^16)。 ​

Webkit源码

image.png Mozilla源码

image.png

十六进制0x10000 / 0x100刚好等于256。 ​

有人做过一个实验,用256个class成功地覆盖了id选择器的样式。不过估计后来Wekit浏览器把base加大了,所以在较新的chrome浏览器上看不到效果。我想知道现在chrome浏览器css优先级计算以多少为base,所以尝试在最新的chrome上用30000多个class选择器去覆盖id选择器,结果页面崩了,哈哈。但可以确定的是,这个base比较大。 ​

理论上说,无论多少个元素类型选择器也不能比1个class选择器的优先级高,无论多少个class选择器也不能比1个id选择器的优先级高。

CSS4草案里补充了这样一句话:

Due to storage limitations, implementations may have limitations on the size of A, B, or C. If so, values higher than the limit must be clamped to that limit, and not overflow.

我理解的意思是,无论多少个class选择器,厂商实现都不能让它的优先级高于1个id选择器。 ​

其实我们也不用太关心浏览器css优先级计算以256还是更大的值为基,因为一般不可能出现连续定义256个class选择器这样的极限情况。 ​

至此,可以得出选择器的优先级比较:

style > id> class = 属性 = 伪类 > 元素类型 = 伪元素 > 通配符

!important

在讲!important之前,先将几个概念:user agent样式、user样式和author样式

  • user agent样式:浏览器提供的默认样式,比如我们使用chrome devtools调试样式时经常看到的样式面板右上角“user agent stylesheet”的字眼,就是这种样式。

image.png

  • author样式:网页开发者提供的样式,也就是前端开发平时写的内联或者外联的css样式
  • user样式:不太常用,就是浏览器允许用户导入自定义的样式表,IE和Firefox支持,Chrome可以通过stylish这个插件支持

有了前面的概念,就可以进入这部分的正题了。 ​

根据CSS3的描述,Cascade Sorting Order规则如下:

  1. css transition
  2. 加了!important的user agent样式
  3. 加了!important的user样式
  4. 加了!important的author样式
  5. css animation
  6. 一般的author样式
  7. 一般的user样式
  8. 一般的user agent样式

顺序越靠前优先级越高。而前面讲的style和选择器样式属于第6项,所以!important的优先级会比style还高。

正常情况下,author样式会覆盖user样式,但有时需要user样式覆盖author样式。为了平衡这个问题,引入important这个标记。一般来说,author样式不需要使用!important

其他优先级规则

  1. 特异性相同,元素的继承样式优先取自离自己最近的父元素
  2. 特异性相同,后加载的样式会覆盖前面加载的样式。举例:

假如有这样一个html:

<!DOCTYPE html>
<head>
  <style>
    .load-before {
      width: 50px;
      height: 50px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div class="load-after load-before"></div>
  <script>
    const head  = document.getElementsByTagName('head')[0];
    const link  = document.createElement('link');
    link.rel  = 'stylesheet';
    link.href = '/public/load-after.css'; // background-color: blue
    head.appendChild(link);
  </script>
</body>

.load-before定义在内联样式里,加载比较快。.load-after定义在外联的css里,通过script动态插入到document,加载比较慢。.load-before.load-after分别定义红色和蓝色的背景色。最终浏览器中会显示蓝色,即使class="".load-after更靠前。所以,样式的优先级跟代码里定义的顺序没有关系,而是跟加载到浏览器或者说浏览器执行的顺序有关系。

总结

通过本文的分析,得出的CSS优先级跟我们以往的认知没有区别,还是!important最大,style次之,然后是id选择器class选择器元素类型选择器通配符等。另外,通过查阅CSS标准、写简单例子验证,使得自己对CSS优先级的理解更进一步了。

参考资料

  1. css-tricks.com/precedence-…
  2. www.thebookdesigner.com/2017/02/sty…
  3. zhuanlan.zhihu.com/p/41604775
  4. www.w3.org/TR/css-styl…
  5. www.w3.org/TR/selector…
  6. www.w3.org/TR/css-casc…
  7. www.w3.org/TR/selector…
  8. stackoverflow.com/questions/1…
  9. ryanseddon.com/css/extreme…
  10. news.ycombinator.com/item?id=438…
  11. www.smashingmagazine.com/2007/07/css…
  12. juicystudio.com/article/sel…
  13. stackoverflow.com/questions/2…
  14. stackoverflow.com/questions/1…
  15. www.thoughtco.com/user-style-…