HTML解析引擎边下载边解析,通过状态机将字符流转为Token,再用栈机制构建DOM树,具备高容错性。

HTML解析引擎不是等整个文件下载完才开始干活,而是边收字节边建节点树——这是理解整个过程的关键前提。
字符流 → Token 是状态机在“读”
浏览器拿到HTTP响应体后,第一件事是按编码(如UTF-8)把字节解成Unicode字符;接着用一个预定义的状态机逐个“吃”字符。遇到 < 就切到“标签开始”状态,后面跟字母就认定为startTag,跟/就转为endTag,遇到空格或换行则可能进入“属性名”或“文本”状态。
常见错误现象:Uncaught DOMException: Failed to execute 'insertAdjacentHTML' on 'Element' 往往是因为传入了未闭合的片段(比如只写了 <div),导致后续Token序列错乱,状态机卡住或误判。
这个阶段不关心语义是否合法,只保证:
- <img src="a.jpg"> 被拆成 startTag(name=“img”,attrs=[{name:“src”, value:“a.jpg”}])
- <!-- comment --> 被识别为 comment 类型Token,直接丢弃
- 连续空白符(含换行)在文本Token中被压缩为单个空格,除非父元素是 <pre> 或设了 white-space: pre
Token → 节点 → DOM树 是栈在“管”嵌套
每个startTag触发新建节点,并压入一个开放元素栈(open element stack);每个endTag则尝试弹出栈顶——但不是无条件弹,而是按HTML规范做容错匹配。例如 <div><p></div> 中,解析器发现 </div> 时栈顶是 p,就会先自动补一个 </p>,再处理 </div>。
立即学习“前端免费学习笔记(深入)”;
这种栈驱动机制决定了:
- <table><tr><td>content</td></tr></table> 实际生成的DOM里,tr 的父节点不是 table,而是浏览器自动插入的 tbody
- <li>one</li><li>two</li> 在没有 <ul> 包裹时,仍会被挂到最近的合法父容器下(比如 body),不会丢失
- 文本节点会被合并:连续两个文本Token(如 "hello" 和 "world" 中间只有空白)会合成一个 Text 节点
DOM树构建不是原子操作,而是渐进可中断的
你无法假设 document.body 在脚本执行时一定存在——因为解析是流式的。如果 <script> 出现在 <body> 中间,且没加 defer 或 async,JS执行时 DOM树只建到那个位置,后面的标签还没解析。
这意味着:
- 在内联脚本里访问 document.getElementById("footer") 可能返回 null,即使它在HTML底部
- 使用 DOMContentLoaded 事件才是安全等待完整DOM树就绪的方式,而不是靠 setTimeout 猜时机
- 服务端用 DOMParser 解析字符串时,行为与浏览器一致,但它是“全量解析”,没有流式中断特性,所以不会出现“一半DOM”的情况
真正容易被忽略的是:解析器对错误的容忍度远高于开发者预期。它不是报错退出,而是靠栈回溯、隐式插入、标签重映射来强行维持树结构有效——这既是HTML鲁棒性的来源,也是写自动化解析工具时必须模拟的逻辑点。



















