理解浏览器如何将任何 HTML 字节流(即便是格式错误的)转换成一棵确定的 DOM 树。
在 Kodokon 中打开本课与 XML 不同,HTML 不存在致命错误:WHATWG 规范逐字节地定义了任何输入流(哪怕格式错误)必须如何变成一棵 DOM 树。解析器分两个阶段工作:词法分析器(tokenizer)将字节流切分成词元(标签、文本、注释),然后树构建器(tree builder)根据一台状态机把它们插入,这些状态被称为插入模式(insertion modes)(in body、in table 等等)。每一个“解析错误”都有标准化的恢复方式:两个符合规范的浏览器会生成完全相同的 DOM。
<table>
<tr><td>Cell</td></tr>
</table>解析器会插入隐式标签:一个 <tbody> 出现在这些行的外围,而 <li>、<p> 或 <option> 会在某些开始标签前自动闭合。更彻底的是:任何在 <table> 内部出现的、本不允许出现的内容会被拉出表格并重新插入到它之前,这个机制称为寄养(foster parenting)。你的 CSS 选择器和脚本始终作用于被修正后的 DOM,而绝非你的源代码。
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>嵌套错误的格式化标签会触发收养机构算法(adoption agency algorithm),这是规范中最刁钻的部分之一:解析器克隆并重新分配活动的格式化元素,以生成一棵正确嵌套的树。下面这段交叉标记展示了结果。
<p>
<b>One <i>two</b> three</i>
</p><b>One <i>two</i></b><i> three</i> —— i 被拆分成了两个元素。解析器是一个流式(streaming)解析器:它随着字节的到来构建这棵树。一个同步的 <script> 会挂起它 —— 脚本可以调用 document.write 并向字节流注入词元 —— 但预加载扫描器(preload scanner)会持续扫描剩余的 HTML,以预加载图片、样式表和脚本。在解析已经结束后调用 document.write 会触发 document.open 并抹掉整个页面:这是这个平台最古老的陷阱之一。
<table><div>Out</div><tr><td>A</td></tr></table> 中,<div> 会怎样?<td> 单元格中<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>,解析器颠倒了嵌套关系