HTML解析器在标签间或标签内捕获非标记字符时自动生成文本节点,依据状态机切换(标签态→数据态),空白按white-space和上下文折叠合并,script/style内容原样保留,文本节点合并发生在DOM构建栈操作阶段。

HTML解析器如何识别和生成文本节点
文本节点不是靠“标签”定义的,而是解析器在两个标签之间、或标签内部捕获到非标记字符时自动创建的。关键在于解析器的状态机何时退出标签状态、进入数据状态。
常见错误现象:document.createTextNode("") 不会出现在原始 HTML 解析中;空格、换行、制表符如果不在 pre 或 textarea 里,大概率被合并或丢弃; 会被转义为一个不可断行空格字符节点,而非普通空格。
- 解析器在遇到
<时进入标签状态,遇到非<字符且不在标签内时,就进入数据状态,持续收集直到下一个< - 连续的空白字符(包括换行)在非预格式化上下文中会被折叠:多个空格 → 1 个空格,首尾空白 → 被 trim,中间换行 → 视为单个空格
-
script和style元素的内容不会被当作普通文本节点处理,而是作为其元素的子节点保留原始内容(不折叠、不转义)
为什么有些空格没进 DOM,有些却进了
这取决于父容器的 CSS white-space 值和 HTML 结构位置。解析器本身不决定是否保留空格,它只是按规则生成文本节点;真正过滤或合并发生在 DOM 构建后期——尤其是相邻文本节点合并阶段。
使用场景:你在 div 里写 Hello<span>World</span>!,中间没有空格,那 DOM 中 Hello 和 World 就是两个独立文本节点,中间无空格;但如果你写 Hello <span>World</span>!(注意 Hello 后有空格),这个空格会成为一个独立文本节点,然后与前后文本节点在构建时被合并成一个节点:"Hello " + "World" → "Hello World"(仅当它们是兄弟且无样式干预时)。
立即学习“前端免费学习笔记(深入)”;
-
white-space: pre或pre元素:所有空白(含换行、缩进)都保留为独立文本节点 -
white-space: normal(默认):解析器仍生成文本节点,但渲染树构建时会合并、折叠;DOM 树里节点存在,只是后续被忽略 - 脚本动态插入文本(如
el.textContent = " a b ")绕过解析器,直接生成未折叠的文本节点
文本节点合并发生在哪一步
不是在词法分析(tokenization)阶段,而是在语法分析(DOM 构建)的栈操作过程中完成的。当解析器连续收到多个文本类 token(比如因换行、空格、文字交替出现),只要它们处于同一父节点下且中间无其他元素节点,就会被合并为一个 Text 节点。
性能影响:过度拆分文本节点(如用 JS 频繁 appendChild(document.createTextNode("x")))会增加 DOM 树深度和遍历开销;但浏览器对纯文本合并非常高效,不必手动预合并。
- 合并只发生在同级、相邻、且类型均为
TEXT的节点之间 -
display: none的父元素不影响文本节点生成,只影响是否进入渲染树 - 注释节点(
<!-- ... -->)和 CDATA 段(<![CDATA[...]]>)也走类似流程,但归为不同 token 类型,不参与文本合并
调试时怎么确认真实文本节点结构
别只信 innerHTML 或开发者工具的“美化视图”,它们会隐藏空白处理逻辑。要用 childNodes 遍历并检查 nodeType:
console.log(Array.from(el.childNodes).map(n => ({
type: n.nodeType === 3 ? 'Text' : n.nodeType === 8 ? 'Comment' : 'Element',
data: n.nodeType === 3 ? `"${n.nodeValue}"` : n.nodeName
})))容易踩的坑:innerText 返回的是渲染后可读文本(已折叠、已隐藏),textContent 才接近原始解析结果(但仍可能受 script 动态修改影响);用 data 属性看值时注意它可能是 null(比如某些注释节点)。
复杂点在于:文本节点的边界受 HTML5 规范中“格式化文本”规则约束,比如 p 内部换行是否生成新节点,取决于它是否被视作“流内容中的空白”。这种判定不是正则能概括的,而是由解析器内置的上下文状态驱动的。



















