4、事件对象封装:事件源、事件目标、事件数据、阻止默认行为与停止传播
事件对象,说白了就是浏览器在触发事件时,悄悄塞给你的那个「包裹」。里面装着谁触发了事件、事件发生在哪个元素上、带了什么数据,以及你能不能干预事件的后续行为。
我刚做前端那会儿,总觉得事件对象就是个参数,用的时候 console.log 看一眼就完事了。直到有一次做拖拽组件,发现 e.target 和 e.currentTarget 搞混了,拖拽逻辑全乱套。嗯,从那以后我才认真把事件对象的每个属性捋了一遍。
4.1 事件源与事件目标
这两个概念最容易混淆。我建议你先记住一句话:事件源是绑定监听器的元素,事件目标是实际触发事件的元素。
| 属性 | 含义 | 冒泡过程中是否变化 |
|---|---|---|
e.currentTarget |
绑定事件监听器的元素(事件源) | 不变 |
e.target |
实际触发事件的元素(事件目标) | 不变 |
核心区别:如果你在父元素上监听点击事件,点击子元素时,e.currentTarget 是父元素,e.target 是子元素。这就是事件委托能工作的底层原理。
// 事件委托示例
document.querySelector('.list').addEventListener('click', function(e) {
// e.currentTarget 始终是 .list
// e.target 是实际被点击的 li
if (e.target.tagName === 'LI') {
console.log('点击了列表项:', e.target.textContent);
}
});
我在项目中遇到过一个问题:用事件委托做表格行点击,结果用户点到了行里的按钮,e.target 变成了按钮而不是行。解决方案很简单——用 e.currentTarget 或者 e.target.closest('tr') 向上查找。
4.2 事件数据
不同类型的事件会携带不同的数据。比如鼠标事件有 clientX/clientY,键盘事件有 keyCode,触摸事件有 touches 数组。
你想想看,如果每次都要自己从 DOM 元素上取数据,那得多麻烦。事件对象把这些数据都封装好了,直接拿来用就行。
| 事件类型 | 常用数据属性 | 说明 |
|---|---|---|
| MouseEvent | clientX, clientY, button |
鼠标坐标、按键编号 |
| KeyboardEvent | key, code, ctrlKey |
按键值、物理键位、修饰键 |
| TouchEvent | touches, changedTouches |
触摸点列表 |
| FocusEvent | relatedTarget |
焦点移入/移出的相关元素 |
小技巧:自定义事件可以通过 new CustomEvent('eventName', { detail: yourData }) 传递任意数据,然后在监听器里用 e.detail 取出来。这是组件间通信的轻量方案。
4.3 阻止默认行为
浏览器的很多元素都有默认行为。比如点击 a 标签会跳转,提交表单会刷新页面,右键会弹出菜单。有时候这些默认行为不是我们想要的。
e.preventDefault() 就是用来干这个的。调用之后,浏览器就不会执行默认动作了。
// 阻止链接跳转
document.querySelector('a.no-nav').addEventListener('click', function(e) {
e.preventDefault();
// 执行自定义逻辑,比如 SPA 路由切换
navigateTo(this.getAttribute('href'));
});
// 阻止表单提交
document.querySelector('#myForm').addEventListener('submit', function(e) {
e.preventDefault();
// 用 AJAX 提交数据
submitFormData(new FormData(this));
});
注意:preventDefault() 只阻止默认行为,不影响事件传播。也就是说,事件该冒泡还是冒泡,该捕获还是捕获。我曾经以为调了 preventDefault 事件就停了,结果父元素的监听器还是触发了,排查了半天。
4.4 停止传播
事件传播是 DOM 事件的核心机制。但有时候你不想让事件继续往上冒泡,或者不想让捕获阶段继续往下走。这时候就需要 e.stopPropagation()。
// 阻止冒泡
child.addEventListener('click', function(e) {
e.stopPropagation();
// 只有子元素的监听器会触发,父元素的不会
console.log('只处理子元素点击');
});
parent.addEventListener('click', function() {
// 如果子元素点击时调用了 stopPropagation,这里不会执行
console.log('父元素点击');
});
还有一个 e.stopImmediatePropagation(),它更狠——不仅阻止传播,还阻止当前元素上其他同类型监听器的执行。
区别总结:
stopPropagation():阻止事件继续传播,但当前元素上绑定的其他同类型监听器仍然会执行stopImmediatePropagation():阻止事件传播,并且当前元素上后续绑定的同类型监听器不再执行
element.addEventListener('click', function(e) {
console.log('第一个监听器');
e.stopImmediatePropagation(); // 第二个监听器不会执行
});
element.addEventListener('click', function() {
console.log('第二个监听器'); // 这行不会输出
});
4.5 完整的事件对象封装结构
下面这张图展示了事件对象的核心结构,我建议你把它记在脑子里。
4.6 实际项目中的封装实践
在实际项目中,我习惯把事件对象的常用操作封装成工具函数。这样既统一了行为,也方便维护。
// 事件工具封装
const EventUtils = {
// 获取事件目标,支持选择器匹配
getTarget(e, selector) {
if (selector) {
return e.target.closest(selector);
}
return e.target;
},
// 安全阻止默认行为
preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false; // IE 兼容
}
},
// 安全停止传播
stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true; // IE 兼容
}
},
// 获取鼠标坐标(兼容各种浏览器)
getMousePosition(e) {
return {
x: e.clientX || e.pageX,
y: e.clientY || e.pageY
};
}
};
避坑指南:我曾经在移动端项目中,因为没处理 touch 事件的 preventDefault,导致页面滚动和手势操作冲突。后来统一在事件入口处判断是否需要阻止默认行为,才彻底解决。
4.7 阻止默认行为 vs 停止传播:什么时候用哪个?
这个问题我经常被问到。其实规则很简单:
- 想阻止浏览器做某件事(比如跳转、刷新、滚动)→ 用
preventDefault() - 想阻止父元素收到事件通知(比如避免冒泡触发父级监听器)→ 用
stopPropagation() - 两者都想阻止 → 两个都调用
重要提醒:不要滥用 stopPropagation()。如果你在某个元素上阻止了冒泡,页面上其他依赖事件委托的功能可能会失效。我见过一个项目,因为到处调用 stopPropagation,导致全局点击统计完全失灵。
事件对象的封装,说白了就是让你能精准控制「谁触发了事件」「事件带了什么」「事件之后会发生什么」。把这几个概念理清楚,你在处理复杂交互时就会游刃有余。