JavaScript DOM 与事件
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
不引入框架时,页面交互全靠 DOM API。用对了很直接,用错了就是「点了没反应」「列表越点越多」这类问题。
环境准备
示例在 Chrome 130 下验证,写法对其他现代浏览器一致。把脚本放在 <body> 末尾,或用 <script type="module">(默认延迟执行),避免拿不到节点。
选择节点
document.querySelector(".card"); // 第一个匹配
document.querySelectorAll(".card li"); // NodeList(静态快照)
document.getElementById("app"); // 单个,最快
const form = document.forms.login; // 具名表单
const input = form.elements.email; // 表单控件
querySelectorAll 返回的 NodeList 是静态的:后续新增的节点不会出现在里面;而 getElementsByClassName 返回 HTMLCollection,是实时的,循环时增删节点容易死循环。优先用 querySelectorAll 再 for...of。
创建与插入
const li = document.createElement("li");
li.textContent = "第一项";
li.classList.add("item");
li.dataset.id = "42";
list.append(li); // 末尾
list.prepend(li); // 开头
li.before(other); // 兄弟位置
li.replaceWith(next);
li.remove();
textContent 插入纯文本,安全;innerHTML 会解析 HTML,拼接用户输入就是 XSS 漏洞:
// 危险
li.innerHTML = `<b>${userInput}</b>`;
// 安全
li.textContent = userInput;
确实需要插入 HTML 结构时,用 <template> + cloneNode,或 el.querySelector() 后再用 textContent 填值。
批量更新
每次改样式或插入节点都可能触发重排。先脱离文档构建,再一次性挂上:
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item.title;
frag.append(li);
}
list.append(frag);
读布局属性(offsetWidth、getBoundingClientRect())会强制同步重排,别在大循环里交替读和写:
// 差:每轮读写交替
for (const el of els) {
el.style.width = el.offsetWidth + 10 + "px";
}
// 好:先读完再写
const widths = els.map((el) => el.offsetWidth);
els.forEach((el, i) => (el.style.width = widths[i] + 10 + "px"));
事件流:捕获、目标、冒泡
点击一个 <button>,事件会从 document 一路捕获下来,到达目标,再冒泡回去。默认监听在冒泡阶段触发:
document.addEventListener("click", () => console.log("document"));
button.addEventListener("click", () => console.log("button"));
// 点击按钮:先 button,后 document
在第三参数打开捕获:
container.addEventListener("click", handler, { capture: true });
常用控制:
event.preventDefault(); // 阻止默认行为(提交、跳转)
event.stopPropagation(); // 阻止继续冒泡(慎用,会破坏委托)
event.target; // 实际触发的元素
event.currentTarget; // 绑定监听的元素
事件委托
列表项是动态生成的,别给每个都绑监听——挂在父容器上,用 target.closest() 判断来源:
list.addEventListener("click", (event) => {
const btn = event.target.closest("button[data-action]");
if (!btn || !list.contains(btn)) return;
const { action } = btn.dataset;
if (action === "delete") btn.closest("li").remove();
});
好处:只一个监听器、动态节点自动生效、内存占用低。注意 closest 要检查祖先边界(上例的 list.contains),否则可能匹配到容器外的同名按钮。
清理监听器
单页应用里组件被替换后监听器仍挂在旧节点上,会泄漏内存,也可能对已销毁状态做出反应。
const controller = new AbortController();
button.addEventListener("click", onClick, { signal: controller.signal });
window.addEventListener("resize", onResize, { signal: controller.signal });
// 卸载时一次性移除
controller.abort();
比记住每个 removeEventListener 的引用更省心;注意移除时不能带 { once: true } 之类的额外选项不一致问题。
表单处理
<form id="login">
<input name="email" type="email" required />
<button type="submit">登录</button>
</form>
form.addEventListener("submit", async (event) => {
event.preventDefault(); // 阻止页面刷新
const data = new FormData(form);
const payload = Object.fromEntries(data);
await fetch("/api/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
});
浏览器自带校验(required、type="email"、pattern)先用起来,别全部手写。提交后给按钮加 disabled 防止重复提交。
事件性能
高频事件(scroll、resize、input)不要直接跑重逻辑,节流或防抖:
function debounce(fn, ms = 200) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
searchInput.addEventListener("input", debounce((e) => search(e.target.value), 250));
input 用防抖(等用户停手),scroll 里的视觉更新用 requestAnimationFrame 合并到每帧一次。
常见错误与注意事项
1. 脚本执行时节点还不存在
document.querySelector("#app") 返回 null,后续 addEventListener 报错。用 type="module"、defer,或把脚本放到 </body> 前。
2. 用 innerHTML 拼用户数据
XSS 的第一入口。文本一律 textContent。
3. event.target 和 this 混用
箭头函数没有自己的 this;用 event.currentTarget 最明确。
4. 循环里绑定闭包变量
// 老问题:全部输出最后一个 i
for (var i = 0; i < 3; i++) {
btn[i].addEventListener("click", () => console.log(i));
}
// 用 let,或事件委托
for (let i = 0; i < 3; i++) {
btn[i].addEventListener("click", () => console.log(i));
}
5. removeEventListener 移除失败
必须传同一个函数引用,匿名函数移不掉。用 AbortController 或具名函数。
6. 直接改 style.cssText 覆盖一切
会清掉之前的内联样式。用 classList 或逐个属性。
7. 忘记 preventDefault 导致页面刷新
表单提交、<a href="#"> 点击都会跳转,调试时先看地址栏是不是变了。
小结
- 查询用
querySelector(All),插入用textContent+DocumentFragment。 - 事件捕获 → 目标 → 冒泡;
target是触发者,currentTarget是监听者。 - 动态列表用事件委托;卸载用
AbortController统一清理。 - 表单先吃浏览器内置校验,提交时
preventDefault并防重复提交。 - 高频事件防抖/节流,读写分离避免强制重排。