在 JavaScript 世界里,事件是交互的核心 ------ 点击按钮、输入文本、鼠标移动,本质上都是事件在驱动。但很多开发者只停留在「会用 onclick」的层面,对事件的触发流程、监听方式、性能优化一知半解。本文就从底层原理出发,结合实战案例,帮你彻底搞懂 JS 事件机制。
一、事件是怎么 "发生" 的?
我们先看一个直观的例子:页面上有一个红色父容器 parent,里面嵌套了一个蓝色子容器 child。当你点击 child 时,到底发生了什么?
html
预览
css
<div id="parent" style="width:200px;height:200px;background:red">
<div id="child" style="width:100px;height:100px;background:blue"></div>
</div>
你可能觉得只是 "点击了蓝色方块",但 JS 的事件系统背后有一套完整的流程 ------事件流(Event Flow),它包含三个阶段:
1. 事件流三阶段(文字图解 + 绘图思路)
核心流程(自上而下→目标→自下而上)
- 捕获阶段:
document→html→body→parent→ 接近child(自上而下缩小范围) - 目标阶段:精准命中
child(event.target就是它) - 冒泡阶段:
child→parent→body→html→document(自下而上扩散)
快速绘图思路(用备忘录 / Figma 2 分钟画好)
- 画一个大矩形代表
document,里面嵌套html(稍小)、body(再小)、parent(红色)、child(蓝色,居中) - 捕获阶段:用「向下的箭头」从
document指向child,标注 "捕获" - 目标阶段:给
child画个圈,标注 "目标元素(event.target)" - 冒泡阶段:用「向上的箭头」从
child指向document,标注 "冒泡"
2. 关键疑问:谁先执行?
如果父元素和子元素都绑定了点击事件,执行顺序完全由「事件阶段」决定:
- 捕获阶段的事件先执行(自上而下)
- 目标阶段的事件次之
- 冒泡阶段的事件最后执行(自下而上)
举个例子验证:
javascript
运行
javascript
// 父元素-捕获阶段
parent.addEventListener('click', () => console.log('parent 捕获'), true);
// 子元素-捕获阶段
child.addEventListener('click', () => console.log('child 捕获'), true);
// 子元素-冒泡阶段
child.addEventListener('click', () => console.log('child 冒泡'), false);
// 父元素-冒泡阶段
parent.addEventListener('click', () => console.log('parent 冒泡'), false);
点击 child 后,控制台输出顺序是:
plaintext
parent 捕获 → child 捕获 → child 冒泡 → parent 冒泡
这就是事件流的核心逻辑:先捕获,再目标,后冒泡。
二、JS 事件的核心特征
1. 事件是异步的
JS 事件的本质是「异步回调」------ 你先注册事件处理函数,它不会立即执行,只有当事件被触发(比如点击、输入)后,才会被加入事件队列,等待主线程空闲时执行。
比如下面的代码,console.log('先执行') 会先输出,点击后才会执行回调:
javascript
运行
javascript
child.addEventListener('click', () => console.log('点击后执行'));
console.log('先执行'); // 先输出
2. 两种事件注册方式(DOM0 vs DOM2)
注册事件的方式有两种,其中 DOM2 级事件是现在的标准,DOM0 级已逐渐被淘汰:
| 特性 | DOM0 级事件(旧) | DOM2 级事件(推荐) |
|---|---|---|
| 语法 | element.onclick = fn |
element.addEventListener(type, fn, useCapture) |
| 事件阶段控制 | 仅支持冒泡阶段 | 支持捕获(useCapture=true)/ 冒泡(false) |
| 多个处理函数 | 不支持(会覆盖) | 支持(可注册多个,按顺序执行) |
| 移除事件 | element.onclick = null |
element.removeEventListener(type, fn, useCapture) |
| 兼容性 | 所有浏览器支持 | IE9+ 支持(IE8 - 用 attachEvent) |
反例(不推荐) :DOM0 级事件会覆盖,模块化差
javascript
运行
ini
// 第二个 onclick 会覆盖第一个
child.onclick = () => console.log('第一个点击事件');
child.onclick = () => console.log('第二个点击事件'); // 仅执行这个
正例(推荐) :DOM2 级事件支持多个处理函数
javascript
运行
javascript
// 两个事件都会执行
child.addEventListener('click', () => console.log('事件1'));
child.addEventListener('click', () => console.log('事件2'));
3. 重要提醒
- 事件监听只能绑定在单个 DOM 元素上 ,不能直接绑定在元素集合上(比如
document.querySelectorAll返回的 NodeList)。 - 事件监听有一定内存开销,尤其是绑定在频繁创建 / 销毁的元素上时,记得及时移除(
removeEventListener),避免内存泄漏。
三、实战技巧:事件委托(性能优化神器)
1. 问题场景
如果页面上有一个列表,里面有 100 个 <li>,需要给每个 <li> 绑定点击事件,该怎么做?
错误做法 :循环遍历所有 <li>,逐个绑定事件
javascript
运行
ini
const lis = document.querySelectorAll('#list li');
lis.forEach(li => {
li.addEventListener('click', () => console.log(li.innerHTML));
});
这种做法的问题:
- 创建 100 个事件处理函数,内存开销大
- 新增
<li>时,需要重新绑定事件,维护成本高
2. 解决方案:事件委托
利用事件的冒泡特性 ,把事件绑定在父元素(比如 <ul>)上,通过 event.target 判断触发事件的子元素,从而实现 "一次绑定,多个元素共用"。
原理 :点击 <li> 时,事件会冒泡到 <ul>,父元素的事件处理函数通过 event.target 拿到真正触发事件的 <li>。
实战代码:
html
预览
xml
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<!-- 可能动态新增 <li> -->
</ul>
<script>
const list = document.getElementById('list');
// 只给父元素绑定一次事件
list.addEventListener('click', (event) => {
// 判断触发事件的是不是 <li>
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.innerHTML);
}
});
// 动态新增 <li>,无需重新绑定事件
const newLi = document.createElement('li');
newLi.innerHTML = '4';
list.appendChild(newLi); // 点击 4 也会触发事件
</script>
3. 事件委托的优势
- 减少事件绑定次数,降低内存开销
- 支持动态元素,新增子元素无需重新绑定
- 简化代码,提高维护性
四、常用事件工具:阻止冒泡、阻止默认行为
1. 阻止事件冒泡(event.stopPropagation())
有时候我们不希望事件冒泡到父元素,比如点击 child 时,只执行 child 的事件,不执行 parent 的事件。
javascript
运行
javascript
parent.addEventListener('click', () => console.log('parent 点击'));
child.addEventListener('click', (event) => {
event.stopPropagation(); // 阻止冒泡
console.log('child 点击');
});
此时点击 child,只会输出 child 点击,parent 的事件不会执行。
2. 阻止默认行为(event.preventDefault())
有些元素有默认行为,比如 <a> 标签点击后跳转、表单提交后刷新页面。我们可以用 preventDefault() 阻止这些默认行为。
html
预览
xml
<!-- 阻止 <a> 标签跳转 -->
<a href="https://juejin.cn" id="link">掘金</a>
<script>
const link = document.getElementById('link');
link.addEventListener('click', (event) => {
event.preventDefault(); // 阻止跳转
console.log('点击了链接,但不跳转');
});
</script>
注意:两者的区别
stopPropagation():阻止事件在事件流中传播(捕获 / 冒泡),但不影响元素自身的默认行为。preventDefault():阻止元素的默认行为,但不影响事件传播。
五、总结
JS 事件机制的核心可以概括为:
- 事件流:捕获 → 目标 → 冒泡(决定事件执行顺序)
- 注册方式 :优先使用 DOM2 级
addEventListener(支持多事件、阶段控制) - 性能优化:利用事件委托,减少事件绑定次数
- 实用工具 :
stopPropagation()阻止传播,preventDefault()阻止默认行为
理解了这些,你就能轻松应对各种交互场景,写出更高效、更易维护的 JS 代码。如果有疑问,欢迎在评论区交流~