7、CSS 解析与样式计算:CSS 解析器(CSSParser)、样式规则匹配、选择器优先级计算、ComputedStyle 的生成
好,我们继续深入 WebKit 的渲染流水线。前面聊了 DOM 树的构建,那是页面的骨架。但光有骨架不行,还得有皮肉和衣服——这就是 CSS 样式。
CSS 解析和样式计算,说白了就是把 CSS 文本变成浏览器能理解的结构,然后算出每个 DOM 节点最终长什么样。这个过程我调试过无数次,每次看到那些奇奇怪怪的样式 bug,最后都能追溯到这几个环节中的某一个。
7.1 CSS 解析器(CSSParser)
CSS 解析器的工作,就是把 CSS 源码变成一棵语法树。WebKit 里用的是 CSSParser 这个类,它和 HTML 解析器是两套独立的系统。
我个人习惯把 CSS 解析分成两个阶段:
- 词法分析:把 CSS 文本拆成 token。比如
color: red会被拆成属性名 token、冒号 token、值 token。 - 语法分析:根据 token 序列构建规则树。这里会处理选择器、声明块、@规则等。
举个例子,下面这段 CSS:
.container {
width: 100%;
background-color: #f0f0f0;
}
解析器会生成一个 StyleRule 对象,里面包含:
- 选择器列表(这里是
.container) - 声明块(两个属性-值对)
我在项目中遇到过一个问题:某些浏览器对 CSS 注释的处理不一致。WebKit 的解析器在遇到注释时会直接跳过,但有些旧版浏览器会把注释内容当成属性值的一部分。嗯,这里要注意,写 CSS 时别在注释里放奇怪的东西。
7.2 样式规则匹配
解析完 CSS 之后,下一步就是把规则应用到 DOM 节点上。这个过程叫样式规则匹配。
WebKit 里有一个关键的数据结构叫 RuleSet,它把所有的 CSS 规则按照选择器类型做了索引。比如:
- ID 选择器:用哈希表存,key 是 ID 值
- 类选择器:也是哈希表,key 是类名
- 标签选择器:按标签名分组
- 通用选择器:单独一个列表
为什么要这么做?你想想看,如果每个 DOM 节点都要遍历所有 CSS 规则,那性能就炸了。有了索引,浏览器可以快速定位到可能匹配的规则。
匹配过程大致是这样的:
- 拿到一个 DOM 节点,比如
<div class="card"> - 从
RuleSet里取出所有可能匹配的规则 - 逐个检查选择器是否真的匹配
- 把匹配的规则收集起来,按优先级排序
我曾经调试过一个性能问题:一个页面有几千个 DOM 节点,每个节点都匹配了上百条规则。结果样式计算花了 800 多毫秒。后来发现是某个开发者用了大量的后代选择器,比如 .wrapper .content .item .title 这种。这种选择器匹配起来特别慢,因为浏览器要沿着 DOM 树往上找祖先节点。
7.3 选择器优先级计算
当多个规则匹配同一个元素时,到底听谁的?这就涉及到选择器优先级了。
WebKit 里用 SelectorSpecificity 这个类来表示优先级。它本质上是一个三元组:
| 权重级别 | 对应选择器 | 示例 |
|---|---|---|
| a(最高) | ID 选择器 | #header |
| b(中等) | 类、属性、伪类选择器 | .nav, [type], :hover |
| c(最低) | 标签、伪元素选择器 | div, ::before |
计算规则很简单:数一下选择器里有多少个 ID、多少个类、多少个标签。比如:
#header .nav a:hover
// a = 1 (一个ID)
// b = 2 (一个类 .nav + 一个伪类 :hover)
// c = 1 (一个标签 a)
// 最终优先级: (1, 2, 1)
比较时先比 a,a 相同比 b,b 相同比 c。如果三个都一样,那就看谁在样式表里出现得更晚。
这里有个容易搞混的地方::not() 伪类本身不计入优先级,但括号里的选择器会计入。比如 :not(#id) 的优先级是 (1, 0, 0),而不是 (0, 1, 0)。
核心要点:优先级计算只看选择器,不看声明的位置。内联样式优先级最高,但它不是通过选择器来算的,而是单独处理的。
7.4 ComputedStyle 的生成
匹配完规则、算好优先级之后,就该生成最终的 ComputedStyle 了。
ComputedStyle 是什么?说白了就是每个 DOM 节点最终渲染时用的样式值。它包含了所有 CSS 属性的最终值,比如 width、height、color、font-size 等等。
生成过程大致分三步:
- 收集匹配的声明:把优先级最高的规则里的声明拿出来
- 处理继承:有些属性(比如
color、font-family)会从父节点继承 - 计算实际值:把相对值转成绝对值,比如把
2em转成32px
WebKit 里 ComputedStyle 是一个很大的结构体,里面按属性分组存储。为了提高性能,它用了共享机制:如果两个 DOM 节点的样式完全一样,它们会指向同一个 ComputedStyle 对象。
我记得有一次排查内存泄漏,发现 ComputedStyle 对象数量异常多。后来发现是因为某个动态生成的元素每次都加了不同的内联样式,导致共享机制失效。嗯,这里要注意,内联样式会阻止样式共享。
7.5 整体流程回顾
为了让你更直观地理解整个过程,我画了一张流程图:
整个流程走下来,你会发现 CSS 解析和样式计算其实是个挺复杂的过程。但 WebKit 做了大量优化,比如样式共享、规则索引、惰性计算等,让这个过程在大多数情况下都能在几毫秒内完成。
最后说一句:理解这些底层机制,对写高性能 CSS 很有帮助。你想想看,当你知道了选择器匹配是怎么工作的,自然就不会写出那种让浏览器跑断腿的选择器了。