JavaScript 事件系统
要点一: 事件系统的三大角色 —— 事件对象(存储状态)、事件源(被操作对象)、监听函数(回调处理)
- 事件最早在 IE3 和 Netscape Navigator 2 中出现,用于分担服务器运算负载
- IE4/Netscape4 时代出现了两种对立的事件流:IE 的冒泡流 vs Netscape 的捕获流
- DOM2 级规范统一了事件模型,形成捕获阶段 → 目标阶段 → 冒泡阶段的三阶段事件流
addEventListener第三个参数useCapture决定在捕获/冒泡阶段触发,默认false(冒泡)stopPropagation()只阻止向其他元素传播,不阻止当前元素的其他监听器stopImmediatePropagation()彻底阻止传播,包括当前元素的后续监听器- 事件代理(委托)利用冒泡特性,将子元素监听统一挂载到父节点,性能更优
事件系统概述
JavaScript 和 HTML 之间的交互通过事件实现。事件是文档或浏览器窗口中发生的特定交互瞬间,通过侦听器(处理程序)监听并执行相应代码。
三大核心角色
| 角色 | 作用 | 示例 |
|---|---|---|
| 事件对象 | 存储事件的状态信息(位置、类型、目标等) | event.target, event.clientX |
| 事件源对象 | 当前被操作的对象 | 元素节点、document、window、XMLHttpRequest |
| 事件监听函数 | 事件触发时执行的回调函数 | function(e) { ... } |
通俗类比:事件源相当于"当事人",监听函数相当于"监护人",事件对象相当于"事故详情"。当事人出事了,详情记录在案,监护人据此做出反应。
历史发展
事件最早在 IE3 和 Netscape Navigator 2 中出现,作为分担服务器运算负载的手段。到 IE4 和 Navigator4 发布时,两种浏览器提 供了相似但不同的 API,并存了多个版本。直到 DOM2 级规范才开始以逻辑方式标准化 DOM 事件。
IE9、Firefox、Opera、Safari 和 Chrome 均已实现"DOM2 级事件"核心模块,IE8 是最后一个使用专有事件系统的主要浏览器。
浏览器事件系统相对复杂:尽管主流浏览器都实现了 DOM2 级事件,但规范本身并未涵盖所有事件类型;BOM 事件与 DOM 事件的关系长期缺乏明确规范(HTML5 后来给出了详细说明);DOM3 级的出现使事件 API 更加繁琐。但核心概念是必须理解的。
事件传播
浏览器发展到第四代(IE4 及 Netscape Communicator4)时,团队遇到了一个关键问题:点击目标元素时,同时也点击了其容器,甚至整个页面。如果这些元素都绑定了点击事件,执行顺序应该是怎样的?

事件流描述页面接收事件的顺序。IE 和 Netscape 团队提出了几乎完全相反的概念:
- IE 事件流:事件冒泡流(由内及外)
- Netscape 事件流:事件捕获流(由外及内)
事件冒泡 vs 事件捕获
| 模式 | 传播方向 | 描述 |
|---|---|---|
| 事件冒泡 | 由内及外 | 最具体的元素先接收,逐级向上传播到不具体的节点(document) |
| 事件捕获 | 由外及内 | 不具体的节点先接收,最具体的节点最后接收 |

所有现代浏览器都支持事件冒泡,但实现略有差异:
- IE5.5 及更早版本跳过
html元素(body 直接到 document) - IE9+、Firefox、Chrome、Safari 将事件一直冒泡到 window 对象
IE9+、Firefox、Chrome、Opera、Safari 均支持事件捕获。尽管 DOM 标准要求事件从 document 开始传播,但这些浏览器都是从 window 对象开始捕获。
由于老版本浏览器不支持事件捕获,实际开发中建议优先使用事件冒泡,仅在特殊需要时使用捕获。
DOM2 级事件流
"DOM2 级事件"规定的事件流包括三个阶段:

- 捕获阶段:事件从
document → html → body向下传播,不涉及实际目标 - 目标阶段:事件在目标元素上发生,事件处理被视为冒泡阶段的一部分
- 冒泡阶段:事件从目标元素向上传播到文档(
div → body → html → document)
- DOM2 级规范明确要求捕获阶段不涉及实际目标,但 IE9+、Chrome、Firefox、Safari、Opera9.5+ 都会在捕获阶段触发目标上的事件,导致目标元素事件执行两次
- 并非所有事件都有冒泡阶段,但所有事件都会经过捕获和目标阶段。例如
focus、blur事件跳过冒泡阶段
典型示例

<div id="wrap">
<div id="outer">
<div id="inner"></div>
</div>
</div>
const wrap = document.getElementById('wrap');
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
Q1: wrap 注册了点击事件,点击哪些 div 会触发?
wrap.addEventListener('click', () => alert('wrap'), false);
A1: wrap、outer、inner —— 事件最终都会冒泡到 wrap。
Q2: 三个元素均在冒泡阶段注册事件,点击 inner 时执行顺序?
wrap.addEventListener('click', () => alert('wrap'), false);
outer.addEventListener('click', () => alert('outer'), false);
inner.addEventListener('click', () => alert('inner'), false);
A2: inner → outer → wrap
Q3: 同一元素同时在捕获和冒泡阶段注册事件,点击 inner 时执行顺序?
// 冒泡阶段
wrap.addEventListener('click', () => alert('wrap bubbling'), false);
outer.addEventListener('click', () => alert('outer bubbling'), false);
inner.addEventListener('click', () => alert('inner bubbling'), false);
// 捕获阶段
wrap.addEventListener('click', () => alert('wrap capture'), true);
outer.addEventListener('click', () => alert('outer capture'), true);
inner.addEventListener('click', () => alert('inner capture'), true);
A3: wrap capture → outer capture → inner bubbling → inner capture → outer bubbling → wrap bubbling
目标元素上的事件按注册顺序执行,与 capture/bubble 参数无关。
stopPropagation()
希望事件到某个节点为止不再传播,使用 stopPropagation():
// 捕获阶段:事件传播到 p 后不再向下
p.addEventListener('click', e => e.stopPropagation(), true);
// 冒泡阶段:事件冒泡到 p 后不再向上
p.addEventListener('click', e => e.stopPropagation(), false);
stopPropagation() 只阻止事件向其他元素传播,不会阻止当前元素的其他 click 监听器。不是彻底取消 click 事件。
p.addEventListener('click', e => {
e.stopPropagation();
console.log(1);
});
p.addEventListener('click', () => {
console.log(2); // 仍然会触发!
});
输出:1 → 2
stopImmediatePropagation()
想要彻底阻止事件传播,包括当前元素的后续监听器,使用 stopImmediatePropagation():
p.addEventListener('click', e => {
e.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', () => {
console.log(2); // 不会触发!
});
输出:1
事件代理(委托)
由于事件在冒泡阶段向上传播到父节点,因此可以将子节点的监听函数定义在父节点上,由父节点统一处理多个子元素的事件。这种方法叫做事件代理(delegation)。
const ul = document.querySelector('ul');
ul.addEventListener('click', e => {
if (e.target.tagName.toLowerCase() === 'li') {
// 处理 li 的点击事件
}
});
优势:
- 只需定义一个监听函数,即可处理多个子节点
- 动态添加的子节点自动生效,无需重新绑定
- 减少内存占用,提升性能
React 合成事件系统正是基于事件代理机制实现的。
事件监听函数
响应某个事件的函数称为事件监听函数。为事件指定处理程序的方式有多种:

由于 HTML 事件监听函数中 HTML 与 JavaScript 紧密耦合,已被大多数程序员摒弃。
HTML 事件监听函数
通过 HTML 特性直接指定:
<!-- 直接写代码 -->
<input type="button" value="Click Me" onclick="alert('clicked!')" />
<!-- 调用函数 -->
<input type="button" value="Click Me" onclick="showMessage()" />
<script>
function showMessage() {
alert('clicked!');
}
</script>
事件触发时会自动创建局部变量 event,可直接访问事件对象:
<!-- 获取目标元素 -->
<input type="button" value="Click Me" onclick="console.log(event.target)" />
<!-- this 指向当前元素 -->
<input type="button" value="Click Me" onclick="console.log(this)" />
缺点:
- 时差问题:用户可能在元素刚出现就触发事件,此时处理函数可能尚未加载
- 耦合问题:HTML 与 JavaScript 紧密耦合,修改需要改动两处
DOM0 级事件监听函数
通过 JavaScript 将函数赋值给事件属性:
<input type="button" id="clicker" value="点击" />
<script>
const clicker = document.getElementById('clicker');
clicker.onclick = function() {
console.log('点击了!');
};
</script>
以这种方式添加的事件,会在事件流的冒泡阶段被处理。
优点:所有浏览器支持,简单,跨浏览器兼容
缺点:绑定事件不能累加,最后绑定的会覆盖之前的
clicker.onclick = function() { alert('第一次'); }; // 被覆盖
clicker.onclick = function() { alert('第二次'); }; // 只执行这个
删除事件:
clicker.onclick = null;
使用 HTML 事件监听函数指定的程序,可以被 DOM0 级事件监听函数覆盖,也可以以同样方式删除。