Veloris.
返回索引
概念基础 2026-03-08

DOM 操作与事件机制

3 分钟
900 words

DOM 操作与事件机制

前言

前几篇我们学了 JS 的语法核心(类型、函数、对象、数组)。但 JS 在浏览器中真正的用途是操控页面——动态修改 HTML 结构、改变样式、响应用户点击和输入。这一切都通过 DOM(Document Object Model,文档对象模型) 实现。

DOM 是浏览器将 HTML 文档解析后生成的树形 API。用嵌入式的话说:DOM 操作就像操作寄存器——读取页面状态、修改页面显示。事件监听就像中断回调——用户点击、输入时触发你预先注册的处理函数。

ℹ️ 学完本篇后请记住:后续学 React 时,你几乎不会直接操作 DOM——React 会帮你管理。但理解 DOM 原理是理解 React 工作方式的基础。

正文

1. DOM 树结构

<html>
  <body>
    <div id="app">
      <h1>标题</h1>
      <p class="text">段落 <a href="#">链接</a></p>
    </div>
  </body>
</html>

对应的 DOM 树:

document
└── html
    └── body
        └── div#app
            ├── h1
            │   └── "标题"
            └── p.text
                ├── "段落 "
                └── a
                    └── "链接"

每个节点都是一个 JS 对象,可以读取属性、调用方法。

2. 查询元素

// 推荐方式(CSS 选择器语法)
document.querySelector("#app");        // 第一个匹配的元素
document.querySelector(".text");       // 第一个 class="text"
document.querySelector("div > h1");    // 组合选择器
document.querySelectorAll(".item");    // 所有匹配的元素(NodeList)

// 旧方式(仍然有效,但不如 querySelector 灵活)
document.getElementById("app");
document.getElementsByClassName("text");   // HTMLCollection(实时更新)
document.getElementsByTagName("p");

// querySelector vs getElementById
// querySelector 返回 null(找不到时)
// getElementById 也返回 null
// querySelectorAll 返回空 NodeList(找不到时)

// 遍历 NodeList
const items = document.querySelectorAll("li");
items.forEach(item => console.log(item.textContent));

// 也可以转为数组
const arr = [...document.querySelectorAll("li")];
arr.filter(li => li.classList.contains("active"));

3. 读取与修改元素

const el = document.querySelector("#app");

// 文本内容
el.textContent;             // 获取纯文本
el.textContent = "新文本";   // 设置纯文本(安全,不解析 HTML)

// HTML 内容
el.innerHTML;               // 获取内部 HTML
el.innerHTML = "<b>加粗</b>"; // 设置 HTML(注意 XSS 风险)

// 属性
const link = document.querySelector("a");
link.getAttribute("href");          // 获取属性
link.setAttribute("href", "/new");  // 设置属性
link.removeAttribute("target");     // 删除属性

// 特殊属性可以直接访问
link.href;
link.id;
link.className;

// data-* 自定义属性
// <div data-user-id="42" data-role="admin">
el.dataset.userId;  // "42"(自动驼峰转换)
el.dataset.role;    // "admin"

// 样式操作
el.style.color = "red";
el.style.fontSize = "20px";         // 驼峰命名(不是 font-size)
el.style.backgroundColor = "#f0f0f0";

// class 操作(推荐,比直接改 style 好)
el.classList.add("active");
el.classList.remove("hidden");
el.classList.toggle("open");         // 有则删,无则加
el.classList.contains("active");     // true/false
el.classList.replace("old", "new");

4. 创建与插入元素

// 创建元素
const div = document.createElement("div");
div.className = "card";
div.textContent = "新卡片";

// 插入到页面
const container = document.querySelector("#app");
container.appendChild(div);                          // 添加到末尾
container.insertBefore(div, container.firstChild);   // 添加到开头
container.prepend(div);                              // 添加到开头(新 API)
container.append(div, "文本也可以");                   // 末尾,支持多个

// insertAdjacentHTML(性能好,常用)
container.insertAdjacentHTML("beforeend", `
  <div class="card">
    <h3>标题</h3>
    <p>内容</p>
  </div>
`);
// 位置参数:beforebegin | afterbegin | beforeend | afterend

// 删除元素
div.remove();                      // 现代方式
container.removeChild(div);        // 旧方式

// 克隆
const clone = div.cloneNode(true); // true = 深克隆(含子元素)

5. 事件监听

const button = document.querySelector("#btn");

// 添加事件监听(推荐方式)
button.addEventListener("click", function(event) {
  console.log("按钮被点击了");
  console.log(event.target);  // 触发事件的元素
  console.log(this);          // 绑定事件的元素(普通函数中)
});

// 箭头函数版本(注意 this 差异)
button.addEventListener("click", (e) => {
  console.log("点击", e.target);
  // 箭头函数中 this 不指向 button,而是外层作用域
});

// 移除事件监听(需要引用同一个函数)
function handleClick() { console.log("clicked"); }
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

// 常用事件类型
// 鼠标:click, dblclick, mouseenter, mouseleave, mousemove
// 键盘:keydown, keyup, keypress(已废弃)
// 表单:submit, input, change, focus, blur
// 窗口:load, resize, scroll
// 触摸:touchstart, touchmove, touchend

6. 事件冒泡与捕获

<div id="outer">
  <div id="inner">
    <button id="btn">点我</button>
  </div>
</div>
// 点击 button 时,事件会"冒泡"向上传播:
// button → #inner → #outer → body → html → document

document.querySelector("#outer").addEventListener("click", () => {
  console.log("outer clicked");
});
document.querySelector("#inner").addEventListener("click", () => {
  console.log("inner clicked");
});
document.querySelector("#btn").addEventListener("click", () => {
  console.log("button clicked");
});

// 点击按钮输出:
// button clicked
// inner clicked
// outer clicked

// 阻止冒泡
document.querySelector("#btn").addEventListener("click", (e) => {
  e.stopPropagation();  // 事件不再向上传播
  console.log("只有我");
});

// 阻止默认行为
document.querySelector("a").addEventListener("click", (e) => {
  e.preventDefault();  // 阻止链接跳转
});

7. ★ 事件委托——性能优化的关键技巧

利用冒泡机制,在父元素上监听子元素的事件:

// 不好的方式:给每个 li 都绑定事件
document.querySelectorAll("li").forEach(li => {
  li.addEventListener("click", () => { /* ... */ });
});
// 如果有 1000 个 li,就创建了 1000 个事件监听器!

// 好的方式:事件委托,只在父元素上绑定一个
document.querySelector("ul").addEventListener("click", (e) => {
  // e.target 是实际被点击的元素
  if (e.target.tagName === "LI") {
    console.log("点击了:", e.target.textContent);
  }
  // 也可以用 closest 查找最近的匹配祖先
  const li = e.target.closest("li");
  if (li) {
    console.log("点击了:", li.textContent);
  }
});
// 只有 1 个事件监听器,动态添加的 li 也能自动响应

事件委托在 React 中被框架自动处理,但理解原理很重要。

💡 工程师手记:事件委托让我想起嵌入式中的中断处理思路——不是给每个外设都配一个中断处理程序,而是用一个统一的中断入口,根据中断源分发到不同的处理函数。事件委托就是这个思路在前端的体现。

(建议替换为你自己理解事件委托的经历)

8. 表单处理

const form = document.querySelector("form");
const input = document.querySelector("#username");

// 获取/设置输入框的值
input.value;             // 获取当前值
input.value = "新的值";   // 设置值

// 监听输入(实时响应每次按键)
input.addEventListener("input", (e) => {
  console.log("当前输入:", e.target.value);
});

// 监听变化(失焦后触发)
input.addEventListener("change", (e) => {
  console.log("最终值:", e.target.value);
});

// 表单提交
form.addEventListener("submit", (e) => {
  e.preventDefault();  // 阻止页面刷新(默认行为)

  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  console.log(data);  // { username: "...", password: "..." }
});

// 键盘事件
document.addEventListener("keydown", (e) => {
  console.log(`按下: ${e.key}, 代码: ${e.code}`);
  if (e.key === "Enter") { /* 回车处理 */ }
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault();  // 阻止浏览器保存
    console.log("自定义保存");
  }
});

总结

知识点核心要点
查询元素querySelector / querySelectorAll(CSS 选择器语法)
读写内容textContent(安全)、innerHTML(可解析 HTML)
样式操作优先用 classList,避免直接写 style
创建插入createElement + append,或 insertAdjacentHTML
事件监听addEventListener,event 对象包含 target/key 等信息
事件冒泡子元素事件向上传播,stopPropagation 阻止
事件委托在父元素上监听,用 e.target 判断实际元素
表单处理e.preventDefault() 阻止刷新,FormData 收集数据

常见问题

💬 你可能会问:innerHTML 和 textContent 用哪个?

默认用 textContent(安全,不解析 HTML)。只有当你确实需要插入 HTML 结构时才用 innerHTML,但要注意 XSS(跨站脚本攻击) 风险——永远不要把用户输入直接拼接到 innerHTML 中。

💬 你可能会问:学了 React 还需要直接操作 DOM 吗?

日常开发中几乎不需要——React 会帮你管理 DOM 更新。但理解 DOM 原理能帮你理解 React 为什么这样设计(比如为什么要用虚拟 DOM),也能帮你在少数需要直接操作 DOM 的场景(如第三方库集成)中游刃有余。

💬 你可能会问:事件监听器会不会导致内存泄漏?

如果元素被移除但事件监听器没有清理,理论上会。但现代浏览器在元素被删除时会自动清理其事件监听器。使用事件委托能从根本上减少监听器数量,是更好的实践。

下一步行动:打开任意网页的 Console,用 document.querySelector 选中一个元素,试着修改它的 textContentclassList。然后给一个按钮加个点击事件——感受一下用 JS 操控页面的力量。

参考资料


📖 系列导航:本文是「FPGA 工程师的前端学习笔记」系列的第 10 篇 上一篇:数组与高阶函数 下一篇:ES6+ 现代语法速览

End of file.