html解析器按字节流而非逐行扫描,tokenizer将输入切分为starttag、endtag、comment、doctype、characterdata五类token,遇暂停移交js引擎执行。

HTML解析器不是“逐行”而是“逐字符流”扫描
浏览器不会按文本编辑器的“行号”去读HTML,parse5、WebKit’s HTMLTokenizer 或 Gecko’s nsHtml5Tokenizer 都把输入当作连续字节流(Uint8Array 或 string),边读边切分 token。所谓“第3行出错”,实际是 tokenizer 在某个 byte offset 处触发了错误状态,和换行符 \n 无直接关系。
常见误解:以为 document.write() 或服务端模板报错提示的“line 12”是解析器在按行工作——其实是 parser 在构造 AST 过程中回溯源码位置时,用 \n 计数生成的调试信息,底层 tokenization 本身无行概念。
tokenization 阶段真正识别的五类基础单元
扫描器(Tokenizer)只做一件事:把字节流转成 startTag、endTag、comment、doctype、characterData 这五种 token。它不关心嵌套、不验证闭合、不处理属性语义。
<div class="a"> → 一个 <code>startTagtoken,name: "div",attributes: [{name: "class", value: "a"}]-
→ 一个endTagtoken,name: "span" -
<!-- hello -->→ 一个commenttoken,data: " hello " → 一个 <code>doctypetoken,name: "html",forceQuirks: false- 纯文本如
Hello world→ 若不在 tag 内,就是characterData;若在<script></script>内,则归为rcdata类型子集 - DOM 构建会阻塞在
<script src="..."></script>处,直到网络请求完成并执行完毕 -
<script>console.log(<div>);</script>不会报错——因为 script 内容被当成 CDATA,tokenizer 完全不解析其中的<div> <li>动态插入的 <code>document.createElement('script')不触发该暂停机制,它的内容由 JS 引擎直接编译,不经过 HTML tokenizer - 用
parse5.Parser+ 自定义treeAdapter - 重写
createElement钩子,在这里拿到原始 tag 名字符串(含大小写)和 source code offset - 避免用
parse5.parseFragment,它跳过 DOCTYPE 和根节点初始化,丢失部分上下文
遇到 <script></script> 时 tokenizer 会暂停并移交控制权
这是最容易被忽略的切换点:当 tokenizer 扫到 <script></script> 开始标签,它会立即停止当前 token 流,把后续字节交给 JavaScript 引擎执行;等脚本执行完(或遇到 ),再恢复扫描。这意味着:
自定义 parser 要绕过默认行为就得 hook createElement
想检测原始大小写标签(比如 <div> 而非标准化后的 <code>div),不能依赖 node.tagName ——那是 tree construction 阶段标准化后的结果。必须在 tokenizer → tree adapter 的衔接点介入:
真正难的不是“怎么扫”,而是“扫完后如何不丢原始形态”。浏览器默认丢弃大小写、合并空白、修正缺失闭合——这些正是手写 parser 时最常漏掉的兼容性细节。











