HTML DOM树解析

DOM树解析

文档对象模型(DOM)是HTML文档的编程接口,它以树形结构表示网页内容,允许开发者通过脚本(如JavaScript)动态访问和修改页面元素。以下从结构解析、节点类型、解析流程及优化等方面展开说明:


一、DOM的树形结构

在HTML文档中,DOM(文档对象模型)树是一种表示文档结构的方式。每个节点代表文档的一部分,如元素、属性或文本。下面是一个简单的HTML示例及其对应的DOM树节点结构图。

一个简单的dom树示意图

假设我们有如下HTML代码:

<!DOCTYPE html>
<html>
<head>
    <title>我的网页</title>
</head>
<body>
    <h1>欢迎来到我的网页</h1>
    <p>这是一个段落。</p>
</body>
</html>

对应的DOM树节点结构可以示意如下:

Document
  └─ html
      ├─ head
      │   └─ title
      │       └─ #text: "我的网页"
      └─ body
          ├─ h1
          │   └─ #text: "欢迎来到我的网页"
          └─ p
              └─ #text: "这是一个段落。"

在这个结构中:

  • Document 是整个文档的根节点。
  • html 节点是HTML文档的根元素。
  • headbody 都是 html 的子节点。
  • title, h1, 和 p 分别是它们各自父节点下的子节点。
  • #text 表示文本节点,包含实际显示在页面上的文字内容。

请注意,实际的DOM树可能会根据具体的HTML代码更加复杂,并且会包含更多类型的节点,比如注释节点、doctype声明等。上述示意图仅为简化版,用于帮助理解DOM树的基本结构。


一个更加复杂的dom树示意图

这里有一个更加复杂的HTML示例及其对应的DOM树节点结构图。这个示例将包含更多种类的元素和结构,例如列表、链接、图片等。

假设我们有如下HTML代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>我的复杂网页</title>
</head>
<body>
    <header>
        <h1>欢迎来到我的复杂网页</h1>
        <nav>
            <ul>
                <li><a href="#home">首页</a></li>
                <li><a href="#about">关于我们</a></li>
                <li><a href="#services">服务</a></li>
                <li><a href="#contact">联系我们</a></li>
            </ul>
        </nav>
    </header>
    <main>
        <section id="home">
            <h2>首页</h2>
            <p>这是首页的内容。</p>
        </section>
        <section id="about">
            <h2>关于我们</h2>
            <p>这是关于我们的内容。</p>
        </section>
        <section id="services">
            <h2>我们的服务</h2>
            <p>这是服务部分的内容。</p>
            <img src="service.jpg" alt="服务图片">
        </section>
        <section id="contact">
            <h2>联系我们</h2>
            <form action="/submit-contact" method="post">
                <label for="email">邮箱:</label>
                <input type="email" id="email" name="email"><br>
                <button type="submit">提交</button>
            </form>
        </section>
    </main>
    <footer>
        <p>版权所有 &copy; 2025 我的公司</p>
    </footer>
</body>
</html>

对应的DOM树节点结构可以示意如下:

Document
  └─ html[lang="zh-CN"]
      ├─ head
      │   ├─ meta[charset="UTF-8"]
      │   └─ title
      │       └─ #text: "我的复杂网页"
      └─ body
          ├─ header
          │   ├─ h1
          │   │   └─ #text: "欢迎来到我的复杂网页"
          │   └─ nav
          │       └─ ul
          │           ├─ li
          │           │   └─ a[href="#home"]
          │           │       └─ #text: "首页"
          │           ├─ li
          │           │   └─ a[href="#about"]
          │           │       └─ #text: "关于我们"
          │           ├─ li
          │           │   └─ a[href="#services"]
          │           │       └─ #text: "服务"
          │           └─ li
          │               └─ a[href="#contact"]
          │                   └─ #text: "联系我们"
          ├─ main
          │   ├─ section[id="home"]
          │   │   ├─ h2
          │   │   │   └─ #text: "首页"
          │   │   └─ p
          │   │       └─ #text: "这是首页的内容。"
          │   ├─ section[id="about"]
          │   │   ├─ h2
          │   │   │   └─ #text: "关于我们"
          │   │   └─ p
          │   │       └─ #text: "这是关于我们的内容。"
          │   ├─ section[id="services"]
          │   │   ├─ h2
          │   │   │   └─ #text: "我们的服务"
          │   │   ├─ p
          │   │   │   └─ #text: "这是服务部分的内容。"
          │   │   └─ img[src="service.jpg", alt="服务图片"]
          │   └─ section[id="contact"]
          │       ├─ h2
          │       │   └─ #text: "联系我们"
          │       └─ form[action="/submit-contact", method="post"]
          │           ├─ label[for="email"]
          │           │   └─ #text: "邮箱:"
          │           ├─ input[type="email", id="email", name="email"]
          │           └─ button[type="submit"]
          │               └─ #text: "提交"
          └─ footer
              └─ p
                  └─ #text: "版权所有 &copy; 2025 我的公司"

在这个更复杂的DOM树中,我们可以看到:

  • HTML文档的基本结构包括<head><body>
  • <header>包含了页面的标题和导航栏。
  • <main>标签内部是主要内容区域,分为几个不同的<section>,每个都有自己的标题和内容。
  • <footer>用于放置页脚信息。

这种结构展示了如何通过嵌套各种HTML标签来构建一个复杂的网页,并且每一个标签都可以看作是DOM树的一个节点。

二、DOM 节点的定义

DOM 树的节点类型由 Node 接口定义。那么,node是什么?

关于node

在网页开发的上下文中,特别是在使用如HTML、XML这些标记语言时,”node”(节点)是构成文档对象模型(DOM, Document Object Model)的基本组成部分。DOM是一种编程接口,用于HTML和XML文档。它表示文档的形式化的方式,允许程序和脚本动态地访问和更新文档的内容、结构及样式。

Node接口是所有节点类型的基础接口,定义了可用于所有类型的节点的方法和属性。

nodeType 是 DOM 节点的一个属性,用于表示节点的类型。每个 DOM 节点都属于特定的类型(如元素、文本、注释等),nodeType 返回一个数字常量,帮助开发者快速判断当前节点的类型。

以下是主要的 nodeType 值及其含义:

常量名说明
1Node.ELEMENT_NODE元素节点(例如 <div><p> 等 HTML 标签)。
2Node.ATTRIBUTE_NODE属性节点(例如 <img src="..."> 中的 src 属性,极少直接操作)。
3Node.TEXT_NODE文本节点(元素内的文本内容,包括空格和换行)。
8Node.COMMENT_NODE注释节点(HTML 中的 <!-- 注释内容 -->)。
9Node.DOCUMENT_NODE文档节点(document 对象,代表整个 HTML 文档的根)。
10Node.DOCUMENT_TYPE_NODE文档类型声明(例如 <!DOCTYPE html>)。
11Node.DOCUMENT_FRAGMENT_NODE文档片段节点(轻量化的临时容器,用于高效批量操作 DOM)。

JavaScript访问


nodeType 的作用

  1. 判断节点类型
    通过 nodeType 可以快速区分不同类型的节点,例如:
   const node = document.querySelector("div");
   if (node.nodeType === Node.ELEMENT_NODE) {
     console.log("这是一个元素节点");
   } else if (node.nodeType === Node.TEXT_NODE) {
     console.log("这是一个文本节点");
   }
  1. 过滤无关节点
    在处理子节点时,可能需要跳过文本节点或注释:
   const parent = document.getElementById("container");
   for (const child of parent.childNodes) {
     if (child.nodeType === Node.ELEMENT_NODE) {
       // 只处理元素节点
       console.log("元素标签名:", child.tagName);
     }
   }

注意事项

兼容性

Node.ELEMENT_NODE 等常量是 Node 接口的静态属性,现代浏览器均支持,但在旧代码中可能直接使用数字(如 1)判断。

代码示例:

// 兼容性写法(推荐使用常量)
if (node.nodeType === 1) { /* 元素节点 */ }

属性节点的特殊性

属性节点(nodeType === 2)虽然存在,但通常不通过 DOM 树的子节点访问,而是通过元素的 attributes 属性:

const img = document.querySelector("img");
const srcAttr = img.attributes.getNamedItem("src");
console.log(srcAttr.nodeType); // 2

空白文本节点

HTML 中的换行或缩进会被解析为文本节点(nodeType === 3),可能包含空字符串:

<div>
  <p>Hello</p>
</div>

<div><p> 之间的换行符会被视为一个文本节点。


DOM 树常见节点类型

DOM 树常见节点类型包括:

文档节点(Document)

  • 表示整个 HTML 文档的根节点,document 对象。
  • nodeType 值为 9

元素节点(Element)

  • 对应 HTML 标签(如 <div><p>)。
  • nodeType 值为 1,可通过 tagName 获取标签名。

属性节点(Attr)

  • 表示元素的属性(如 <img src="image.jpg"> 中的 src)。
  • nodeType 值为 2,属于元素节点的子节点,但一般不直接操作。

文本节点(Text)

  • 包含元素的文本内容(如 <p>Hello</p> 中的 Hello)。
  • nodeType 值为 3,没有子节点。

注释节点(Comment)

  • 对应 HTML 注释(<!-- comment -->)。
  • nodeType 值为 8

文档片段节点(DocumentFragment)

  • 轻量级的文档片段,用于高效批量操作 DOM。
  • nodeType 值为 11

三、DOM 树的解析流程

浏览器接收到HTML文档后,逐行解析并生成Token,随后构建DOM树。若遇到<script>标签,会暂停解析并执行脚本(同步加载),可能导致页面渲染延迟。

浏览器解析 HTML 并构建 DOM 树的步骤如下:

词法分析(Tokenization)

  • 将 HTML 字符串分解为标记(tokens),如开始标签 <div>、结束标签 </div>、属性、文本等。

tokens是什么?

在处理HTML文档时,将HTML字符串分解为标记(tokens)是解析过程中的一个重要步骤。Tokens可以理解为构成HTML文档的基本单元,这些基本单元能够帮助解析器理解和构建文档的结构。

具体来说,当我们将HTML字符串分解为tokens时,通常会得到以下几种类型的tokens:

  1. 标签开始(Start Tag):比如<div>,表示一个元素的开始。
  2. 标签结束(End Tag):例如</div>,表示之前开始的元素在此处结束。
  3. 自闭合标签(Self-closing Tag):如<img src="..." />,这种标签不需要对应的结束标签。
  4. 属性(Attributes):出现在开始标签内,提供有关元素的额外信息,例如<a href="http://example.com">中的href="http://example.com"
  5. 文本(Text):元素之间的实际内容,例如<p>这里是段落文本。</p>中的“这里是段落文本。”
  6. 注释(Comment):以<!--开头,以-->结尾的内容,不会在浏览器中显示。

通过将HTML文档分解成这些基础的tokens,解析器能够更高效地处理和操作文档。这个过程是实现动态网页内容更新、SEO优化以及各种前端框架功能的基础。例如,在React中,JSX语法最终会被转换成相应的HTML tags tokens以便渲染到页面上。此外,理解这一过程有助于开发者更好地掌握前端技术,提升网页开发技能。

构建节点(Node Creation)

  • 根据标记创建对应的 DOM 节点。例如,遇到 <div> 会创建元素节点,遇到文本内容会创建文本节点。

树形结构生成

  • 通过栈结构维护节点层级关系:
    • 遇到开始标签时,创建元素节点,并作为当前节点的子节点。
    • 遇到结束标签时,弹出栈顶元素,回到父级上下文。
  • 例如,解析以下 HTML:
<html>
  <body>
    <h1>Title</h1>
    <p>Paragraph</p>
  </body>
</html>

生成的 DOM 树结构为:

Document
└── html
    └── body
        ├── h1
        │   └── "Title" (文本节点)
        └── p
            └── "Paragraph" (文本节点)

容错处理

  • 浏览器会自动修复不规范的 HTML(如未闭合的标签),确保生成合法的 DOM 树。

构建完成与后续流程

DOM树构建完成后,浏览器会触发DOMContentLoaded事件,随后结合CSSOM(CSS对象模型)生成渲染树(Render Tree),进入布局(Layout)和绘制(Paint)阶段。


四、JavaScript访问

节点关系与遍历

每个节点通过以下属性关联其他节点:

  • parentNode: 父节点。
  • childNodes: 子节点列表(类数组对象)。
  • firstChild / lastChild: 第一个/最后一个子节点。
  • previousSibling / nextSibling: 前一个/后一个兄弟节点。

示例:遍历子节点

const element = document.querySelector('div');
for (const child of element.childNodes) {
  if (child.nodeType === Node.ELEMENT_NODE) {
    console.log('元素节点:', child.tagName);
  } else if (child.nodeType === Node.TEXT_NODE) {
    console.log('文本内容:', child.textContent);
  }
}

关键注意事项

  1. 空白文本节点
    若 HTML 中存在换行或缩进,浏览器可能生成空的文本节点(可通过 nodeType 过滤)。
  2. 脚本阻塞解析
    遇到 <script> 标签时,浏览器会暂停解析并执行脚本(除非标记为 asyncdefer)。
  3. 动态修改 DOM
    通过 JavaScript 动态修改 DOM(如 appendChild()innerHTML)会触发浏览器的重解析(reflow/repaint)。
  4. 优化策略
  • <script>标签置于<body>末尾,或使用async/defer属性实现异步加载。
  • 使用document.createElement动态添加元素,减少初始解析负担。

DOM操作与性能优化

  1. 常见操作
    • 通过getElementByIdquerySelector等方法获取节点。
    • 使用appendChildremoveChild增删节点。
    • 修改节点属性或样式(如element.style.color)。
  2. 性能影响
    DOM操作可能触发重排(Reflow)重绘(Repaint),频繁操作会降低性能。优化方法包括:
  • 使用DocumentFragment批量操作节点。
  • 通过classList一次性修改多个样式,减少重排次数。

DOM与事件机制

DOM树的事件传播遵循捕获阶段 → 目标阶段 → 冒泡阶段。可通addEventListener绑定事件,并利用事件委托(Event Delegation)优化性能,例如将事件监听器绑定到父节点而非多个子节点。