Kodokon kodokon.com

从 HTML 到 DOM:解析与错误恢复

理解浏览器如何将任何 HTML 字节流(即便是格式错误的)转换成一棵确定的 DOM 树。

10 分钟 · 3 题

在 Kodokon 中打开本课

与 XML 不同,HTML 不存在致命错误:WHATWG 规范逐字节地定义了任何输入流(哪怕格式错误)必须如何变成一棵 DOM 树。解析器分两个阶段工作:词法分析器(tokenizer)将字节流切分成词元(标签、文本、注释),然后树构建器(tree builder)根据一台状态机把它们插入,这些状态被称为插入模式(insertion modes)in bodyin table 等等)。每一个“解析错误”都有标准化的恢复方式:两个符合规范的浏览器会生成完全相同的 DOM。

HTML
<table>
  <tr><td>Cell</td></tr>
</table>
生成的 DOM 中包含一个你从未写过的 tbody。

解析器会插入隐式标签:一个 <tbody> 出现在这些行的外围,而 <li><p><option> 会在某些开始标签前自动闭合。更彻底的是:任何在 <table> 内部出现的、本不允许出现的内容会被拉出表格并重新插入到它之前,这个机制称为寄养(foster parenting)。你的 CSS 选择器和脚本始终作用于被修正后的 DOM,而绝非你的源代码。

JAVASCRIPT
const html =
  "<table><div>out</div><tr><td>ok</td></tr></table>";
const doc = new DOMParser()
  .parseFromString(html, "text/html");
console.log(doc.body.innerHTML);
// <div>out</div><table>...</table>
用 DOMParser 观察到的寄养现象。

嵌套错误的格式化标签会触发收养机构算法(adoption agency algorithm),这是规范中最刁钻的部分之一:解析器克隆并重新分配活动的格式化元素,以生成一棵正确嵌套的树。下面这段交叉标记展示了结果。

HTML
<p>
  <b>One <i>two</b> three</i>
</p>
生成的 DOM:<b>One <i>two</i></b><i> three</i> —— i 被拆分成了两个元素。

解析器是一个流式(streaming)解析器:它随着字节的到来构建这棵树。一个同步的 <script> 会挂起它 —— 脚本可以调用 document.write 并向字节流注入词元 —— 但预加载扫描器(preload scanner)会持续扫描剩余的 HTML,以预加载图片、样式表和脚本。在解析已经结束后调用 document.write 会触发 document.open 并抹掉整个页面:这是这个平台最古老的陷阱之一。

知识检测

确认你已牢记本课的重点内容。

  1. <table><div>Out</div><tr><td>A</td></tr></table> 中,<div> 会怎样?
    • 它会被静默地从 DOM 中移除
    • 它会被寄养机制移动到表格之前
    • 它会被包裹进一个为它创建的 <td> 单元格中
    • 它会保持为表格的直接子节点
  2. HTML 规范如何处理一份格式错误的文档?
    • 它让每个浏览器自行选择恢复策略
    • 它要求切换到怪异模式(quirks mode)
    • 它定义了一套确定的恢复算法,在所有符合规范的浏览器中完全一致
    • 它要求在遇到第一个错误时停止解析
  3. 片段 <p><div>X</div></p> 会产生怎样的 DOM?
    • <p></p><div>X</div><p></p>:div 关闭了第一个 p,而那个孤立的 </p> 创建了第二个空的 p
    • <p><div>X</div></p>,原样保留
    • <div><p>X</p></div>,解析器颠倒了嵌套关系