返回笔记列表
01 · NOTES / WebGIS 开发

第 15 节 · 事件

2026 年 9 月 1 日

第 15 节 · 事件

📌 版本信息:基于 DOM Living Standard / UI Events(2026-08-29 核对) 📚 来源:现代 JS 教程 · 浏览器事件简介现代 JS 教程 · 事件委托MDN 事件参考

一、这一节的目标

  1. 掌握 addEventListener 的标准用法与常见事件类型
  2. 理解事件对象 e:里面装着"谁、在哪、点了什么"
  3. 精通冒泡与捕获——事件传播的两段旅程
  4. 精通事件委托——列表/地图类应用的性能与简洁之道
  5. 了解表单事件与键盘事件

二、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, '条');
});

三大好处:

  1. 监听数量从 N → 1(性能)
  2. 动态新增的子项自动生效——不用给新元素补挂监听(第 14 节练习里 innerHTML 重画后事件依然好使,就是委托的功劳)
  3. 代码集中,逻辑好维护

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

步骤:

  1. Live Server 打开:一个"城市收藏夹",能点星标、能删除、能悬停高亮
  2. 读代码:整页只有一个 click 监听(挂在 ul 上),全部靠委托分发
  3. 完成 4 个 TODO:新增城市后无需补挂监听(验证委托威力)、双击改名、Ctrl+点击多选删除、键盘 Enter 聚焦搜索框
  4. 实验:把删除按钮的事件里加 e.stopPropagation(),观察会发生什么异常(理解为什么委托体系里慎用)

通关标准:

  • 新增的城市行点击立即生效(没有补挂任何监听)
  • 能画出"点击 → 捕获 → 目标 → 冒泡 → 委托处理"的旅程图
  • 能说出 e.targete.currentTarget 的区别

八、自测题

  1. 为什么统一用 addEventListener 而不是 onclick
  2. 事件的默认传播顺序是哪两段?日常监听发生在哪段?
  3. 事件委托解决了哪三个问题?
  4. e.targete.currentTarget 什么时候相同、什么时候不同?
  5. e.preventDefault()e.stopPropagation() 分别阻止什么?

参考答案

  1. 可挂多个同类监听、有对称的解绑 API、可配置捕获阶段等选项。
  2. 捕获(外→内)→ 目标 → 冒泡(内→外);默认监听在冒泡段(或目标段)。
  3. 监听数量 N→1 的性能、动态子项自动生效、逻辑集中好维护。
  4. 监听直接挂在目标元素上时两者相同;委托模式下 currentTarget 是挂监听的父元素,target 是实际被点的子孙。
  5. preventDefault 阻止浏览器默认行为(表单提交、链接跳转);stopPropagation 阻止事件继续向上冒泡(与默认行为无关)。

九、下一步

同步的世界快完结了——但真实数据要花时间到达 → 第 16 节:异步——回调与 Promise,跨进 JS 最重要的一道门。

TAGSweb