浏览器幕后 02:CSSOM 与样式计算
本系列 《浏览器幕后:渲染流水线深度解构》 第二篇。
目标:搞懂“浏览器如何决定某个元素最终长什么样”。
1. 回顾:渲染流水线中的“样式计算”
在上一篇我们学习了六步全景图:
HTML → DOM
CSS → CSSOM
Style(样式计算) ← 本文主角
Layout
Paint
Composite
样式计算 的任务是:
把 CSS 规则(可能来自多个来源:作者写的、浏览器默认的、继承的)和 DOM 树结合起来,为每一个元素算出最终生效的样式值。
大白话:
- DOM 像演员名单(谁上台)
- CSSOM 像服装规则册(每类演员穿什么)
- 样式计算就是给每个演员发最终服装(可能有多条规则冲突,要仲裁)
2. CSSOM 是什么?
CSSOM = CSS Object Model,是浏览器把 CSS 解析成的一棵样式规则树。
.card { width: 200px; }
.card .title { font-size: 20px; }
会被解析成类似这样的结构(简化):
Rule 1: 选择器 ".card" → { width: 200px }
Rule 2: 选择器 ".card .title" → { font-size: 20px }
注意:CSSOM 和 DOM 是两棵独立的树,样式计算阶段才会把它们关联起来。
3. 样式计算三连问
对每个 DOM 元素,浏览器会依次回答三个问题:
- 匹配:哪些 CSS 规则命中了这个元素?
- 层叠:如果有多个规则命中同一个属性,谁胜出?
- 计算:最终值是多少(把相对值、百分比换算成绝对值)?
这就像法官判案:先找出相关法条 → 冲突时按优先级裁决 → 最后确定具体刑期。
4. 选择器匹配:从右向左的秘密
以选择器 div .item span 为例。
浏览器通常从右向左匹配:先找所有 <span>,然后检查它的父元素是否有 class="item",再向上检查是否有 div。
为什么从右向左?
- 右侧选择器(
span)能快速缩小候选集合(比从左向右遍历全部元素更高效)。 - 避免大量无效匹配。
对新手的影响:
- 不必过度担心“写了一个后代选择器就会性能崩坏”,现代浏览器已高度优化。
- 但在超大页面(几千个元素)中,过于复杂的选择器(如
div ul li a span)仍会增加匹配成本。
5. 层叠(Cascade):冲突时谁赢?
当多个规则命中同一属性,浏览器按以下顺序(从高到低)裁决:
| 优先级顺序 | 说明 | 例子 |
|---|---|---|
1. 来源 + !important | 用户 !important > 作者 !important > 浏览器默认 !important | 尽量少用 |
| 2. 内联样式 | style="color:red" | 权重高,但不推荐滥用 |
| 3. 选择器优先级 | ID > class > 标签 | #id 胜于 .class |
| 4. 书写顺序 | 后写的覆盖先写的 | 同优先级时 |
优先级(specificity)快速计算法:
- 内联样式:加 1000
- ID 选择器(
#id):加 100 - class、属性、伪类(
.class、[type="text"]、:hover):加 10 - 标签、伪元素(
div、::before):加 1
记忆:千内百类十标签。
5.1 冲突判定小剧场(超实用)
HTML:
<h2 id="title" class="card">标题</h2>
CSS:
.card { color: blue; } /* 优先级 = 10 */
#title { color: red; } /* 优先级 = 100 */
.card { color: green; } /* 优先级 = 10,后写 */
最终颜色:红色
因为 #title 优先级(100)最高,后面绿色的 .card 无法覆盖。
记住:先比优先级,再比先后顺序。优先级高的一票否决。
6. 继承:父元素的样式会传给子元素吗?
部分属性会继承,部分不会。
| 常继承 | 常不继承 |
|---|---|
color | width |
font-family | margin |
line-height | padding |
text-align | border |
例:
.parent { color: blue; border: 1px solid red; }
子元素会变蓝色,但不会自动有红色边框。
继承的值可以被子元素自己的样式覆盖(优先级规则同样适用)。
7. 计算值:从“50%”到“200px”
你写的:
.child {
width: 50%;
padding: 20px;
box-sizing: content-box;
}
浏览器样式计算阶段会做:
- 确定父容器宽度(假设 800px)
width: 50%→ 计算值 = 400pxpadding: 20px→ 无百分比,保持 20px- 结合
box-sizing: content-box→ 总宽度 = 400 + 20*2 = 440px
如果改成 border-box,则总宽度固定为 400px,内容区压缩到 360px。
这就是为什么“你写的 CSS 很短,结果却依赖上下文”。
8. 依赖链:为什么改一个父元素,子元素全变了?
如果子元素宽度是 %,父元素宽度也是 %,就会形成依赖链,最终追溯到视口(viewport)或根元素。
body { width: 90%; }
.parent { width: 80%; } /* 相对 body */
.child { width: 50%; } /* 相对 .parent */
最终 .child 的宽度 = 视口宽度 × 0.9 × 0.8 × 0.5。
这就是为什么修改一个父容器,下面多层都会重算(重排)。
9. 新手高频误区清单
| 误区 | 正解 |
|---|---|
| 后写的样式一定覆盖先写的 | 优先级更高的规则可以压过后写的 |
| 百分比宽度绝对可靠 | 它依赖父容器宽度,父变子变 |
样式没生效就加 !important | 先查命中与优先级,再决定 |
| 只看自己写的 CSS 文件 | 浏览器默认样式(User Agent)、组件库样式也可能影响 |
| 子元素会自动继承父元素所有样式 | 只有部分属性继承(如 color,不继承 margin) |
10. 5 分钟动手实验(强烈建议)
目标:亲眼看到“优先级 > 先后顺序”。
- 创建一个 HTML 文件:
<!DOCTYPE html>
<html>
<head>
<style>
#title { color: red; }
.card { color: blue; }
.card { color: green; } /* 后写,但优先级低 */
</style>
</head>
<body>
<h2 id="title" class="card">标题</h2>
</body>
</html>
- 用浏览器打开,打开 DevTools → Elements → 选中
<h2>→ Styles 面板。- 你会看到
color: red生效(来自#title),.card的蓝色和绿色都被划掉。
- 你会看到
- 把
#title那行注释掉,再看:此时.card最后一条绿色生效(同优先级,后写覆盖先写)。 - 恢复
#title,加上!important观察。
做完这一步,你对层叠的理解会非常稳。
11. 结合你当前水平的调试建议
遇到样式不生效,按这个顺序排查:
- 规则命中了吗?
Elements → Styles → 看右侧是否有对应的规则(灰色可能被覆盖)。 - 被谁覆盖了?
找到划线的规则,看上面哪条优先级更高或后写。 - 最终值是多少?
Computed 面板,搜索属性,看实际值。 - 这个值依赖谁?
看父容器尺寸、box-sizing、媒体查询条件。
DevTools 是你最好的老师,不要只靠猜。
12. 小练习:检验你是否真的懂了
<div class="parent" style="width: 600px;">
<div class="child">内容</div>
</div>
.parent { width: 800px; } /* 内联样式会覆盖它吗? */
.child {
width: 50%;
padding: 10px;
box-sizing: border-box;
}
问题:
.child的内容区宽度是多少?- 如果把
box-sizing改成content-box,总宽度变成多少? - 内联样式
style="width:600px"和 CSS 规则.parent { width:800px }哪个生效?为什么?
答案(先思考,再查看):
- 父容器宽度 = 600px(内联优先级高),
.child宽度 = 50% = 300px(border-box 包含 padding),内容区宽度 = 300 - 20 = 280px。 - 改成
content-box:内容区 = 300px,总宽度 = 300 + 20 = 320px。 - 内联样式生效,因为优先级高于 class 选择器。
13. 下一篇预告
浏览器幕后 03:视觉格式化模型与布局
- 包含块(Containing Block)—— 定位的参照系
- BFC(块级格式化上下文)—— 边距折叠与清除浮动的本质
- 常规流、浮动、绝对定位的布局算法
我们会用大白话 + 可运行示例,带你彻底搞懂“元素的位置是怎么算出来的”。
💬 互动:你在写 CSS 时,有没有遇到过“明明我写了样式,但它就是不生效”的怪事?评论区分享你的案例,下一篇我可能用它做反面教材。