CSSOM 与样式计算:浏览器如何算出每个元素的最终样式

19 阅读7分钟

浏览器幕后 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 元素,浏览器会依次回答三个问题:

  1. 匹配:哪些 CSS 规则命中了这个元素?
  2. 层叠:如果有多个规则命中同一个属性,谁胜出?
  3. 计算:最终值是多少(把相对值、百分比换算成绝对值)?

这就像法官判案:先找出相关法条 → 冲突时按优先级裁决 → 最后确定具体刑期。


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. 继承:父元素的样式会传给子元素吗?

部分属性会继承,部分不会。

常继承常不继承
colorwidth
font-familymargin
line-heightpadding
text-alignborder

.parent { color: blue; border: 1px solid red; }

子元素会变蓝色,但不会自动有红色边框。

继承的值可以被子元素自己的样式覆盖(优先级规则同样适用)。


7. 计算值:从“50%”到“200px”

你写的:

.child {
  width: 50%;
  padding: 20px;
  box-sizing: content-box;
}

浏览器样式计算阶段会做:

  1. 确定父容器宽度(假设 800px)
  2. width: 50% → 计算值 = 400px
  3. padding: 20px → 无百分比,保持 20px
  4. 结合 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 分钟动手实验(强烈建议)

目标:亲眼看到“优先级 > 先后顺序”。

  1. 创建一个 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>
  1. 用浏览器打开,打开 DevTools → Elements → 选中 <h2> → Styles 面板。
    • 你会看到 color: red 生效(来自 #title),.card 的蓝色和绿色都被划掉。
  2. #title 那行注释掉,再看:此时 .card 最后一条绿色生效(同优先级,后写覆盖先写)。
  3. 恢复 #title,加上 !important 观察。

做完这一步,你对层叠的理解会非常稳。


11. 结合你当前水平的调试建议

遇到样式不生效,按这个顺序排查:

  1. 规则命中了吗?
    Elements → Styles → 看右侧是否有对应的规则(灰色可能被覆盖)。
  2. 被谁覆盖了?
    找到划线的规则,看上面哪条优先级更高或后写。
  3. 最终值是多少?
    Computed 面板,搜索属性,看实际值。
  4. 这个值依赖谁?
    看父容器尺寸、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;
}

问题

  1. .child 的内容区宽度是多少?
  2. 如果把 box-sizing 改成 content-box,总宽度变成多少?
  3. 内联样式 style="width:600px" 和 CSS 规则 .parent { width:800px } 哪个生效?为什么?

答案(先思考,再查看):

  1. 父容器宽度 = 600px(内联优先级高),.child 宽度 = 50% = 300px(border-box 包含 padding),内容区宽度 = 300 - 20 = 280px。
  2. 改成 content-box:内容区 = 300px,总宽度 = 300 + 20 = 320px。
  3. 内联样式生效,因为优先级高于 class 选择器。

13. 下一篇预告

浏览器幕后 03:视觉格式化模型与布局

  • 包含块(Containing Block)—— 定位的参照系
  • BFC(块级格式化上下文)—— 边距折叠与清除浮动的本质
  • 常规流、浮动、绝对定位的布局算法

我们会用大白话 + 可运行示例,带你彻底搞懂“元素的位置是怎么算出来的”。


💬 互动:你在写 CSS 时,有没有遇到过“明明我写了样式,但它就是不生效”的怪事?评论区分享你的案例,下一篇我可能用它做反面教材。