浏览器的事件体系是前端交互的核心。理解事件流的三个阶段和事件委托的原理,是编写高性能代码的基础。
1. DOM 事件流
当你在页面上点击一个按钮时,事件并不是只发生在按钮上,而是在元素节点与根节点之间按照特定的顺序传播。这个过程称为 事件流。
根据 W3C 标准,事件流分为三个阶段:
三个阶段
- 捕获阶段 (Capturing Phase):事件从
window对象开始,向下经过document、html、body,直到到达目标节点的父节点。目的是在事件到达预定目标之前捕获它。 - 目标阶段 (Target Phase):事件到达实际触发事件的元素(
e.target)。 - 冒泡阶段 (Bubbling Phase):事件从目标节点开始,沿 DOM 树向上回溯,直到
window对象。目的是让父元素有机会响应子元素的事件。
注意事项
- 默认状态:大多数事件(如
click)默认在冒泡阶段触发。 - 不可冒泡事件:所有事件都要经过捕获阶段和处于目标阶段,但并非所有事件都会冒泡,例如
focus(获得焦点)、blur(失去焦点)、mouseenter、mouseleave。 - 监听设置:
addEventListener(type, handler, useCapture)的第三个参数useCapture若为true,则在捕获阶段执行;默认为false(冒泡阶段执行)。
1.1 原始事件模型 (DOM Level 0)
绑定方式:
- 通过 HTML 属性绑定:
<input type="button" onclick="fun()"> - 通过 DOM 属性绑定:
btn.onclick = fun;
- 通过 HTML 属性绑定:
特性:
- 绑定速度快:直接操作 DOM 属性。
- 唯一性:同一个类型的事件只能绑定一次,后绑定的会覆盖之前的。
- 局限性:只支持冒泡,不支持捕获。
示例:
javascript
var btn = document.getElementById('btn');
btn.onclick = fun1;
btn.onclick = fun2; // 出错:后绑定的事件会覆盖掉之前的事件- 移除事件:将对应事件属性置为
null。
javascript
btn.onclick = null;1.2 标准事件模型 (DOM Level 2)
遵循 W3C 标准,包含完整的事件流阶段。在同一个 DOM 元素上绑定多个事件处理器不会产生冲突。
阶段说明:
- 事件捕获:从
document向下传播到目标元素,检查并执行捕获阶段监听函数。 - 事件处理:到达目标元素,触发目标元素的监听函数。
- 事件冒泡:从目标元素向上回溯到
document,检查并执行冒泡阶段监听函数。
- 事件捕获:从
核心 API:
addEventListener(eventType, handler, useCapture):绑定监听。removeEventListener(eventType, handler, useCapture):移除监听。
参数说明:
eventType:事件类型(不加on)。handler:处理函数。useCapture:是否在捕获阶段执行,默认false(冒泡阶段执行)。
示例:
javascript
var btn = document.getElementById('btn');
// 绑定多个不冲突
btn.addEventListener('click', showMessage1, false);
btn.addEventListener('click', showMessage2, false);
// 移除
btn.removeEventListener('click', showMessage1, false);2. 事件委托 (Event Delegation)
原理
事件委托是利用事件冒泡机制,将子元素的事件监听器绑定到其父元素(或更高层级)上。当子元素被点击时,事件会冒泡到父元素,由父元素的监听器通过检查 event.target 来处理。
性能优势
- 减少内存消耗:不需要为成百上千个子元素分别绑定监听器,只需绑定一个到父元素。
- 动态绑定:对于新增的子元素,无需重新绑定事件,父元素的监听器依然有效。
- 减少 DOM 操作:降低了修改 DOM 树时带来的性能开销。
应用场景
最经典的场景是列表(<ul>)下的列表项(<li>)点击处理。
javascript
const list = document.querySelector('#myList');
list.addEventListener('click', function(e) {
// 检查点击的是否是 li 标签
if (e.target && e.target.nodeName === 'LI') {
console.log('List item clicked:', e.target.innerText);
}
});解释
事件冒泡过程中上传到父节点,父节点通过事件对象获取到目标节点,把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理子元素的事件。 比如100个li,每个都有click,如果使用for遍历 添加事件,关系页面整体性能,需要不断交互 访问dom次数过多,引起重排,延长交互时间。 事件委托的话,将操作放进JS,只需要和dom交互一次,提高性能,还节约内存。 第三个参数(useCapture)为true在捕获过程执行,反之在冒泡过程执行。
3. 事件对象常用 API
在处理事件时,event 对象提供了几个关键控制方法:
- e.target:真正触发事件的元素(在委托中很有用)。
- e.currentTarget:绑定监听器的元素(始终是父级或本身)。
- e.stopPropagation():阻止事件继续向上冒泡。
- e.stopImmediatePropagation():阻止冒泡,并阻止该元素上绑定的其他后续监听器执行。
- e.preventDefault():阻止默认行为(如 A 标签跳转、表单提交)。
总结:熟练掌握事件流能帮你处理复杂的 UI 交互嵌套,而善用事件委托则是优化长列表性能的不二法门。