【重塑】DOM 与 CSSOM 的构建艺术:HTML 字符串如何变成内存中的树形结构?

6 阅读3分钟

在《浏览器底层手记》的上一篇中,我们目睹了字节流如何穿过网络森林抵达渲染进程。现在,这些原始的 二进制字节(Bytes) 已经躺在了渲染引擎的入口。

要把一串死板的字符变成内存中鲜活的 DOM(文档对象模型) 树,浏览器需要经历一场极其严密的“工业化流水线”处理。


【重塑】DOM 与 CSSOM 的构建艺术

一、 五步炼金术:从字节到节点

渲染引擎的主线程(Main Thread)并不能直接理解 HTML 字符串,它必须通过以下五个步骤进行转化:

  1. 解码(Conversion): 浏览器根据指定的编码(如 UTF-8)将原始字节转回为字符串。

  2. 令牌化(Tokenization): 这是最神奇的一步。解析器会将字符串拆解为一个个不可分割的“令牌”。

    • 例如:<html> 变成 StartTag: htmlHello 变成 Character 令牌。
  3. 词法分析与节点生成: 每个令牌会被立即转换成一个 Node(节点) 对象。

  4. 树构建(Tree Construction): 浏览器根据 HTML 规范建立父子、兄弟关系。

  5. 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 是渲染进程中最重要的两棵树。但请记住,它们此时还只是内存中的逻辑结构,并没有位置、没有颜色,更没有出现在屏幕上。

这些“灵魂”需要一次深度的结合,才能化为肉眼可见的图形。