深入理解 DOM

1 阅读5分钟

DOM(文档对象模型)详解:前端开发的核心基础

什么是 DOM?

文档对象模型(Document Object Model,简称 DOM) 是 HTML 和 XML 文档的编程接口。它通过将文档的结构以对象树的形式存储在内存中,将网页与脚本或编程语言连接起来。

简单来说,DOM 就是浏览器将 HTML 文档解析成的一个树状结构,每个 HTML 元素都是树中的一个节点(Node),我们可以通过 JavaScript 等脚本语言来操作这些节点,从而改变网页的结构、样式和内容。

DOM 的核心概念

DOM 树结构

DOM 使用逻辑树的形式来表示文档。树的每个分支末端都是一个节点,每个节点都包含对象。例如,考虑如下 HTML 文档:

<html lang="zh-CN">
  <head>
    <title>我的文档</title>
  </head>
  <body>
    <h1>标题</h1>
    <p>段落内容</p>
  </body>
</html>

其对应的 DOM 树结构如下:

html
├── head
│   └── title
│       └── "我的文档"
└── body
    ├── h1
    │   └── "标题"
    └── p
        └── "段落内容"

DOM 节点类型

DOM 树中的每个节点都有特定的类型,常见的节点类型包括:

  • 元素节点(Element Node):HTML 标签,如 <div><p><span>
  • 文本节点(Text Node):元素内的文本内容
  • 属性节点(Attribute Node):元素的属性,如 classid
  • 文档节点(Document Node):整个文档的根节点
  • 注释节点(Comment Node):HTML 注释

DOM 和 JavaScript

虽然 DOM 不是 JavaScript 语言的一部分,但它通常与 JavaScript 一起使用。DOM 提供了访问和操作网页的标准接口,而 JavaScript 则提供了操作 DOM 的能力。

基本示例

// 获取所有段落元素
const paragraphs = document.querySelectorAll("p");

// 访问第一个段落
console.log(paragraphs[0].nodeName); // 输出: "P"

// 修改第一个段落的内容
paragraphs[0].textContent = "新的段落内容";

// 添加样式
paragraphs[0].style.color = "red";

常用的 DOM 操作方法

1. 选择元素

querySelector 和 querySelectorAll
// 选择第一个匹配的元素
const element = document.querySelector(".my-class");//class为my-class的第一个元素
const elementById = document.querySelector("#my-id");//id为my-id的元素

// 选择所有匹配的元素(返回 NodeList)
const elements = document.querySelectorAll("p");
getElementById、getElementsByClassName、getElementsByTagName
// 通过 ID 获取元素
const element = document.getElementById("my-id");

// 通过类名获取元素集合(返回 HTMLCollection)
const elements = document.getElementsByClassName("my-class");

// 通过标签名获取元素集合
const paragraphs = document.getElementsByTagName("p");

2. 创建和添加元素

// 创建新元素
const newDiv = document.createElement("div");
newDiv.textContent = "新创建的元素";
newDiv.className = "new-class";

// 添加到页面
const parent = document.querySelector(".container");
parent.appendChild(newDiv);

// 插入到指定位置
const referenceElement = document.querySelector(".reference");
parent.insertBefore(newDiv, referenceElement);

3. 修改元素内容

const element = document.querySelector("#my-element");

// 修改文本内容(推荐,更安全)
element.textContent = "新的文本内容";

// 修改 HTML 内容(注意 XSS 风险)
element.innerHTML = "<strong>加粗文本</strong>";

// 修改属性
element.setAttribute("class", "new-class");
element.id = "new-id";

4. 删除元素

const element = document.querySelector("#to-remove");

// 方法1:使用 remove() 方法(现代浏览器)
element.remove();

// 方法2:使用 removeChild() 方法
const parent = element.parentNode;
parent.removeChild(element);

5. 修改样式

const element = document.querySelector("#my-element");

// 直接修改 style 属性
element.style.color = "red";
element.style.backgroundColor = "blue";
element.style.fontSize = "16px";

// 添加/移除类名(推荐方式)
element.classList.add("active");
element.classList.remove("inactive");
element.classList.toggle("highlight");

DOM 事件处理

DOM 事件是用户与网页交互时触发的动作,如点击、鼠标移动、键盘输入等。

添加事件监听器

const button = document.querySelector("#my-button");

// 方法1:使用 addEventListener(推荐)
button.addEventListener("click", function(event) {
  console.log("按钮被点击了");
  console.log("事件对象:", event);
});

// 方法2:使用箭头函数
button.addEventListener("click", (event) => {
  console.log("按钮被点击了");
});

// 方法3:使用命名函数
function handleClick(event) {
  console.log("按钮被点击了");
}
button.addEventListener("click", handleClick);

移除事件监听器

// 必须使用相同的函数引用
button.removeEventListener("click", handleClick);

事件传播

DOM 事件有两个阶段:

  1. 捕获阶段(Capture Phase):事件从文档根节点向下传播到目标元素
  2. 冒泡阶段(Bubble Phase):事件从目标元素向上传播到文档根节点
// 阻止事件冒泡
button.addEventListener("click", function(event) {
  event.stopPropagation();
  console.log("事件不会继续向上传播");
});

// 阻止默认行为
const link = document.querySelector("a");
link.addEventListener("click", function(event) {
  event.preventDefault();
  console.log("链接的默认跳转行为被阻止");
});

常用事件类型

  • 鼠标事件clickdblclickmousedownmouseupmousemovemouseovermouseout
  • 键盘事件keydownkeyupkeypress
  • 表单事件submitchangeinputfocusblur
  • 窗口事件loadresizescroll

DOM 遍历

访问父节点和子节点

const element = document.querySelector("#my-element");

// 父节点
const parent = element.parentNode;

// 子节点
const children = element.childNodes; // 包括文本节点
const childrenElements = element.children; // 只包括元素节点

// 第一个和最后一个子节点
const firstChild = element.firstChild;
const lastChild = element.lastChild;

// 第一个和最后一个子元素
const firstElementChild = element.firstElementChild;
const lastElementChild = element.lastElementChild;

// 兄弟节点
const nextSibling = element.nextSibling;
const previousSibling = element.previousSibling;

实际应用示例

示例1:动态创建列表

// HTML 结构
// <ul id="my-list"></ul>

const list = document.getElementById("my-list");
const items = ["项目1", "项目2", "项目3"];

items.forEach((itemText) => {
  const li = document.createElement("li");
  li.textContent = itemText;
  li.addEventListener("click", function() {
    console.log(`点击了: ${itemText}`);
  });
  list.appendChild(li);
});

示例2:表单验证

const form = document.querySelector("#my-form");
const emailInput = document.querySelector("#email");

form.addEventListener("submit", function(event) {
  event.preventDefault();
  
  const email = emailInput.value;
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  
  if (!emailRegex.test(email)) {
    alert("请输入有效的邮箱地址");
    emailInput.focus();
    return;
  }
  
  // 提交表单
  console.log("表单验证通过,可以提交");
  // form.submit();
});

示例3:动态加载内容

async function loadUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    
    // 更新 DOM
    document.querySelector("#user-name").textContent = user.name;
    document.querySelector("#user-email").textContent = user.email;
    document.querySelector("#user-avatar").src = user.avatar;
  } catch (error) {
    console.error("加载用户数据失败:", error);
  }
}

DOM 性能优化建议

1. 缓存 DOM 查询结果

// ❌ 不好:每次都查询 DOM
for (let i = 0; i < 100; i++) {
  document.querySelector("#my-element").textContent = i;
}

// ✅ 好:缓存查询结果
const element = document.querySelector("#my-element");
for (let i = 0; i < 100; i++) {
  element.textContent = i;
}

2. 使用 DocumentFragment 批量操作

// ✅ 使用 DocumentFragment 减少重排
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const li = document.createElement("li");
  li.textContent = `项目 ${i}`;
  fragment.appendChild(li);
}
document.querySelector("#my-list").appendChild(fragment);

3. 使用事件委托

// ❌ 不好:为每个元素添加事件监听器
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", handleClick);
});

// ✅ 好:使用事件委托
document.querySelector("#container").addEventListener("click", function(event) {
  if (event.target.classList.contains("item")) {
    handleClick(event);
  }
});

4. 避免频繁操作样式

// ❌ 不好:每次修改都触发重排
element.style.width = "100px";
element.style.height = "100px";
element.style.backgroundColor = "red";

// ✅ 好:使用类名或一次性修改
element.className = "new-style";
// 或
element.style.cssText = "width: 100px; height: 100px; background-color: red;";

DOM 与现代前端框架

虽然现代前端框架(如 Vue、React、Angular)提供了更高级的抽象层,但理解 DOM 仍然非常重要:

  1. 框架底层仍然操作 DOM:所有框架最终都要操作 DOM
  2. 调试和优化:理解 DOM 有助于调试和性能优化
  3. 原生 JavaScript 开发:在不使用框架的场景下,直接操作 DOM 是必需的
  4. 学习其他 API:许多 Web API 都基于 DOM 概念

总结

DOM 是前端开发的核心基础,它提供了:

  • 文档的树状结构表示:将 HTML 文档转换为可操作的对象树
  • 标准化的操作接口:统一的方法来访问和修改文档
  • 事件处理机制:响应用户交互的能力
  • 跨浏览器兼容性:标准化的 API 确保代码在不同浏览器中工作

掌握 DOM 操作是每个前端开发者的必备技能。虽然现代框架提供了更便捷的开发方式,但深入理解 DOM 原理仍然非常重要,它有助于我们写出更高效、更可靠的代码。

参考资源