在《浏览器底层手记》的上一篇中,我们目睹了字节流如何穿过网络森林抵达渲染进程。现在,这些原始的 二进制字节(Bytes) 已经躺在了渲染引擎的入口。
要把一串死板的字符变成内存中鲜活的 DOM(文档对象模型) 树,浏览器需要经历一场极其严密的“工业化流水线”处理。
【重塑】DOM 与 CSSOM 的构建艺术
一、 五步炼金术:从字节到节点
渲染引擎的主线程(Main Thread)并不能直接理解 HTML 字符串,它必须通过以下五个步骤进行转化:
-
解码(Conversion): 浏览器根据指定的编码(如
UTF-8)将原始字节转回为字符串。 -
令牌化(Tokenization): 这是最神奇的一步。解析器会将字符串拆解为一个个不可分割的“令牌”。
- 例如:
<html>变成StartTag: html,Hello变成Character令牌。
- 例如:
-
词法分析与节点生成: 每个令牌会被立即转换成一个 Node(节点) 对象。
-
树构建(Tree Construction): 浏览器根据 HTML 规范建立父子、兄弟关系。
-
DOM 树诞生: 最终形成一个以
Document为根节点的树形数据结构。
二、 容错机制:浏览器的“缝补”能力
HTML 是一门非常宽容的语言。即使你写错了代码,浏览器也会尽力帮你纠正,这就是为什么 HTML 永远不会报“语法错误”。
- 自动补全: 如果你忘了写
</body>或</html>,解析器在读到文档末尾时会自动补全它们。 - 纠正嵌套: 如果你写了
<b><i>Hello</b></i>,浏览器会自动调整为正确的嵌套结构。 - 插入缺失标签: 如果你在
<table>里直接写文字,浏览器会帮你生成一个<tbody>和<tr><td>。
底层视角: 这种容错机制极大地增加了解析器的复杂性,也是 HTML 解析无法通过简单的正则表达式完成的原因。
三、 性能的拦截者:阻塞与预解析
在构建 DOM 的过程中,并不是一帆风顺的。
1. 脚本阻塞(Script Blocking)
当解析器遇到 <script> 标签时,它会立刻停止 DOM 的构建,去下载并执行 JS。
- 原因: JS 可能会通过
document.write()改变 DOM 结构,如果边解析边执行,会导致混乱。 - 优化: 使用
async(异步加载,加载完执行)或defer(异步加载,DOM 解析完执行)。
2. CSS 阻塞(Wait for CSSOM)
虽然 CSS 不会直接阻塞 DOM 解析,但它会阻塞 JS 的执行。如果 JS 要查询某个元素的颜色,它必须等到 CSSOM(CSS 对象模型)构建完成。
3. 救星:预解析器(Preload Scanner)
既然遇到脚本会阻塞,那剩下的 HTML 怎么办?
浏览器会开启一个辅助线程(Preload Scanner)。它会快速扫描后面的 HTML,发现有外部资源(图片、脚本、样式表)时,提前发起网络请求,而不是等到主线程解析到那里才去下载。
四、 另一个主角:CSSOM 的构建
在构建 DOM 的同时,浏览器也在构建 CSSOM(CSS Object Model) 。
- 级联(Cascading): CSSOM 最核心的任务是确定每个节点的样式。由于存在全局样式、类选择器、内联样式,浏览器需要计算出最终的优先级(Specificity)。
- 开销: 计算 CSS 属性是非常耗时的。如果你使用了过于复杂的选择器(例如
.a .b .c .d ...),浏览器需要从右向左递归查找,性能开销随之增加。
五、 开发者工具里的真相
打开 Chrome DevTools 的 Performance 面板:
- Parse HTML: 这就是 DOM 构建的时间。
- Recalculate Style: 这就是 CSSOM 构建并应用到节点的时间。
结语
DOM 和 CSSOM 是渲染进程中最重要的两棵树。但请记住,它们此时还只是内存中的逻辑结构,并没有位置、没有颜色,更没有出现在屏幕上。
这些“灵魂”需要一次深度的结合,才能化为肉眼可见的图形。