事件冒泡与委托
理解捕获/冒泡机制、事件委托优化
事件冒泡与委托
点击一个按钮,不只是触发了这个按钮的事件,还会一层一层向上传递给所有祖先元素——这就是冒泡。理解它,你就能用"事件委托"用一个监听器处理成百上千个元素。
学完本章你将: 理解捕获/冒泡流程、用事件委托减少监听器、自定义事件。
事件传播 —— 点击一个元素发生了什么
当你点击嵌套结构中的内层元素时,事件会经历三个阶段:
捕获阶段 ↓(从外到内)
document → html → body → #outer → #inner
↓ 目标阶段(到达目标)
冒泡阶段 ↑(从内到外)
#inner → #outer → body → html → document
// 默认 addEventListener 在冒泡阶段触发
document.querySelector("#outer").addEventListener("click", () => {
console.log("outer 冒泡"); // 后执行
});
document.querySelector("#inner").addEventListener("click", () => {
console.log("inner(目标)"); // 先执行
});
// 点击 inner:先 inner 后 outer
// 捕获阶段(第三个参数 true)
document.querySelector("#outer").addEventListener("click", () => {
console.log("outer 捕获"); // 先于冒泡执行
}, true);
阻止传播
document.querySelector("#inner").addEventListener("click", (e) => {
e.stopPropagation(); // 到此为止,不再向上冒泡
console.log("只有我执行");
});
// stopImmediatePropagation —— 更"狠"
el.addEventListener("click", (e) => {
e.stopImmediatePropagation();
console.log("第一个");
});
el.addEventListener("click", () => {
console.log("不会执行"); // 被阻止了
});
事件委托 —— 用一个监听器管理一切
传统方式给每个子元素绑定事件——动态添加的元素没事件。事件委托利用冒泡,在父元素上监听,通过 e.target 判断实际点击的是谁。
// ❌ 每个元素一个监听器,新元素没有
document.querySelectorAll(".item").forEach(item => {
item.addEventListener("click", handler);
});
// ✅ 事件委托:只在父元素上监听
document.querySelector("#list").addEventListener("click", (e) => {
let item = e.target.closest(".item"); // 找到最近的 .item 祖先
if (item) {
console.log("点击:", item.textContent);
}
});
// 动态添加的元素自动获得"事件处理"!
list.insertAdjacentHTML("beforeend", '<li class="item">新项目</li>');
💡 核心价值: 列表有 1000 行 → 1 个监听器 vs 1000 个。动态增删元素不需要重新绑定。这是前端性能优化的基本功。
let list = document.querySelector("#list"); list.addEventListener("click", (e) => { // 判断点击的是否是目标元素 if (e.target.matches(".item")) { console.log("点击了:", e.target.textContent); } // 或者用 closest 查找祖先 let item = e.target.closest(".item"); if (item && list.contains(item)) { console.log("委托处理:", item.textContent); } });
// 新添加的元素自动获得事件处理能力 let newItem = document.createElement("li"); newItem.className = "item"; newItem.textContent = "新项目"; list.appendChild(newItem);
---
## 事件委托实战
```javascript
// 菜单委托
document.querySelector("#menu").addEventListener("click", (e) => {
let action = e.target.dataset.action;
switch (action) {
case "edit": handleEdit(e.target.closest("tr")); break;
case "delete": handleDelete(e.target.closest("tr")); break;
case "view": handleView(e.target.closest("tr")); break;
}
});
// 表单输入委托
document.querySelector("#form").addEventListener("input", (e) => {
if (e.target.matches("[data-validate]")) {
validateField(e.target);
}
});
自定义事件
// 创建自定义事件
let customEvent = new CustomEvent("userLogin", {
detail: { userId: 123, name: "Alice" },
bubbles: true,
});
// 监听
document.addEventListener("userLogin", (e) => {
console.log("用户登录:", e.detail.name);
});
// 触发
document.dispatchEvent(customEvent);