第 15 节 · 事件
第 15 节 · 事件
📌 版本信息:基于 DOM Living Standard / UI Events(2026-08-29 核对) 📚 来源:现代 JS 教程 · 浏览器事件简介 | 现代 JS 教程 · 事件委托 | MDN 事件参考
一、这一节的目标
- 掌握
addEventListener的标准用法与常见事件类型 - 理解事件对象
e:里面装着"谁、在哪、点了什么" - 精通冒泡与捕获——事件传播的两段旅程
- 精通事件委托——列表/地图类应用的性能与简洁之道
- 了解表单事件与键盘事件
二、addEventListener:监听的一切起点
const btn = document.querySelector('#save');
btn.addEventListener('click', function (e) {
console.log('被点了', e);
});
// 同一元素可挂多个同类监听(都会执行)
btn.addEventListener('click', handler2); // 不会覆盖上面那个
// 解绑(要传同一个函数引用)
btn.removeEventListener('click', handler2);
对比老写法 btn.onclick = fn:只能挂一个、解绑麻烦——统一用 addEventListener。
常用事件清单(按场景记):
| 类别 | 事件 | 用途 |
|---|---|---|
| 鼠标 | click / dblclick / contextmenu(右键) |
交互主力 |
| 鼠标 | mousemove / mouseenter / mouseleave |
悬浮提示(地图 tooltip 雏形) |
| 表单 | submit / input / change |
搜索框实时过滤、下拉选择 |
| 键盘 | keydown / keyup |
快捷键、方向键 |
| 页面 | DOMContentLoaded / load |
脚本初始化时机 |
💡 地图库的事件就是同一套思想换了名字:
map.on('click', handler)(Leaflet)——第 05 模块你会无缝衔接。
三、事件对象 e:事件的全家福信息
document.addEventListener('click', function (e) {
e.target // 真正被点的最深元素
e.currentTarget // 当前挂监听的元素(= 函数里的 this)
e.clientX / e.clientY // 鼠标在视口中的坐标(弹窗跟随鼠标的基础)
e.key // 键盘事件:按的是哪个键('Enter'、'Escape'、'ArrowUp')
e.preventDefault(); // 阻止默认行为(最常用:阻止表单刷新页面、阻止右键菜单)
e.stopPropagation(); // 阻止事件继续冒泡(下面讲)
});
表单提交三件套你已经用过:form.addEventListener('submit', (e) => { e.preventDefault(); … })——现在你知道 preventDefault 阻止的是"浏览器原生提交跳转"。
四、冒泡与捕获:事件的旅程
点击页面上的 <span> 时,这个事件不是凭空发生的,它要走两段旅程:
window
└─ document
└─ <body> ↓ 捕获阶段:从外向内(很少用)
└─ <ul>
└─ <li> 🎯 目标阶段:真正的当事人
└─ <ul> ↑ 冒泡阶段:从内向外(默认监听在这段)
└─ <body>
└─ window
- 冒泡(默认):事件从目标元素一路向上"冒"给每个祖先——你在
ul上挂 click,点它里面的li也会触发(e.target仍是最深的那个) e.stopPropagation():半路掐断冒泡(用了它,祖先的监听收不到——慎用,会破坏委托)
这个"设计缺陷般的行为"恰恰是下一节的性能法宝。
五、事件委托:一夫当关
需求:1000 条列表项,每条都要响应点击。笨办法是给 1000 个 li 各挂一个监听;聪明办法是只给父元素 ul 挂一个,利用冒泡统一处理:
document.querySelector('#list').addEventListener('click', function (e) {
const li = e.target.closest('li'); // 从实际点击处向上找最近的 li
if (!li) return; // 点的不是列表内容,忽略
const id = Number(li.dataset.id);
console.log('操作了第', id, '条');
});
三大好处:
- 监听数量从 N → 1(性能)
- 动态新增的子项自动生效——不用给新元素补挂监听(第 14 节练习里 innerHTML 重画后事件依然好使,就是委托的功劳)
- 代码集中,逻辑好维护
closest(selector) 是委托的好搭档:从 e.target 向上找最近的匹配祖先,找不到返回 null。
💡 地图视角:Leaflet 的图层点击、OL 的
forEachFeatureAtPixel,思想同源——都是"一层监听,分发到具体要素"。
六、data-* 属性:给元素挂自定义数据
<li data-id="42" data-type="quake">武汉 M5.1</li>
const li = e.target.closest('li');
li.dataset.id // '42'(⚠️ 字符串!要 Number() 转换)
li.dataset.type // 'quake'
HTML 里写 data-小写名,JS 里用 dataset.驼峰名 读。它是"页面元素 ↔ 数据记录"之间最轻量的桥梁,事件委托几乎总跟它搭档。
七、动手跟练:15 · 事件委托列表
配套文件:02-web-basics/examples/15-事件委托列表.html
步骤:
- Live Server 打开:一个"城市收藏夹",能点星标、能删除、能悬停高亮
- 读代码:整页只有一个 click 监听(挂在 ul 上),全部靠委托分发
- 完成 4 个 TODO:新增城市后无需补挂监听(验证委托威力)、双击改名、Ctrl+点击多选删除、键盘 Enter 聚焦搜索框
- 实验:把删除按钮的事件里加
e.stopPropagation(),观察会发生什么异常(理解为什么委托体系里慎用)
通关标准:
- 新增的城市行点击立即生效(没有补挂任何监听)
- 能画出"点击 → 捕获 → 目标 → 冒泡 → 委托处理"的旅程图
- 能说出
e.target与e.currentTarget的区别
八、自测题
- 为什么统一用
addEventListener而不是onclick? - 事件的默认传播顺序是哪两段?日常监听发生在哪段?
- 事件委托解决了哪三个问题?
e.target和e.currentTarget什么时候相同、什么时候不同?e.preventDefault()和e.stopPropagation()分别阻止什么?
参考答案
- 可挂多个同类监听、有对称的解绑 API、可配置捕获阶段等选项。
- 捕获(外→内)→ 目标 → 冒泡(内→外);默认监听在冒泡段(或目标段)。
- 监听数量 N→1 的性能、动态子项自动生效、逻辑集中好维护。
- 监听直接挂在目标元素上时两者相同;委托模式下 currentTarget 是挂监听的父元素,target 是实际被点的子孙。
- preventDefault 阻止浏览器默认行为(表单提交、链接跳转);stopPropagation 阻止事件继续向上冒泡(与默认行为无关)。
九、下一步
同步的世界快完结了——但真实数据要花时间到达 → 第 16 节:异步——回调与 Promise,跨进 JS 最重要的一道门。