05事件基础
DOM 让 JS 能「看到」页面上每一个元素——事件则让 JS 能「听到」用户在做什么:
点击、输入、滚动、键盘、鼠标移动……前四章我们学的是「静态取元素、改属性」,
这一课起开始让页面真正活起来。addEventListener 是 DOM 事件的核心 API,
事件冒泡 / 捕获机制与事件委托则是面试高频题。
一、addEventListener 三参数
给元素注册监听器的标准 API 是
el.addEventListener(type, handler, options)。三个参数:
type 是事件名字符串('click' / 'input' / 'keydown' 等),
handler 是事件发生时被调用的函数(自动收到一个
Event 对象),options 是控制监听行为的对象或布尔。
| 分类 | 事件名 | 触发时机 | 典型用法 |
|---|---|---|---|
| 鼠标 | click / dblclick / mousedown / mouseup / mousemove / mouseenter / mouseover |
点击、双击、按下/抬起、移动、进入/悬停 | 下拉菜单、拖拽、悬浮提示 |
| 键盘 | keydown / keyup / keypress |
键盘按下、抬起、按下产生字符 | 回车提交、快捷键、游戏控制 |
| 表单 | input / change / submit / focus / blur / reset |
实时输入、失焦后变化、提交、聚焦、失焦、重置 | 实时搜索、表单校验、登录注册 |
| window | load / resize / scroll / hashchange / popstate |
页面加载完成、窗口尺寸变化、滚动、URL 哈希变化 | 自适应布局、回到顶部、SPA 路由 |
| document | DOMContentLoaded / visibilitychange |
DOM 解析完成(不必等图片)、标签页切换前后台 | 提早绑定事件、切后台暂停视频 |
onclick 属性(el.onclick = ...)也能监听点击,
但同一事件只能写一个,多个会被覆盖。addEventListener
则可以叠加多个监听器,现代项目一律用它。二、event 对象:target vs currentTarget
handler 收到的 Event 对象里有两个最容易混的属性:
event.target 是实际被点的元素,
event.currentTarget 是绑监听的元素(等同于 handler 里的 this)。
因为事件会冒泡,所以二者经常不一样——这一区别是写「事件委托」的前提。
| 属性 | 含义 | 常见用途 |
|---|---|---|
event.target | 实际触发事件的那个元素(可能在内层) | 判断「用户到底点的谁」 |
event.currentTarget | 绑监听的元素(handler 内的 this) | 知道「当前是哪个元素在响应」 |
this | 同 currentTarget(仅普通函数,箭头函数见第六节) | — |
event.target 与 event.currentTarget。把监听改绑到内层按钮上再看输出捕获阶段(从 window 一路向目标)→ 目标阶段(命中元素本身)→
冒泡阶段(从目标一路冒回 window)。默认 addEventListener
在冒泡阶段触发,所以内层的事件会"自动传给"外层——这是事件委托能成立的根本原因。三、stopPropagation vs preventDefault
event 对象上两个方法经常让人混淆——
stopPropagation() 阻止事件继续冒泡或捕获,
preventDefault() 阻止元素的默认行为(链接跳转、表单提交等)。
它俩互不相干——一个管「事件流」,一个管「默认动作」。
stopPropagation:
那会破坏外层组件的正常监听。
只有确认「冒泡会让外层做错事」时再用,常见于自己组件的最外层。四、事件冒泡 vs 捕获
addEventListener 的第三个参数控制「触发时机」:
false(默认)= 冒泡阶段触发,
true = 捕获阶段触发。
同一元素可以两个阶段都注册,
执行顺序是:捕获 → 目标 → 冒泡(像石头落水:先沉到底、再浮上来)。
| 阶段 | 走向 | 第三参数 | 典型用途 |
|---|---|---|---|
| 1. 捕获 | window → ... → 目标的父 | true(或 {capture: true}) | 「先于子元素」做拦截(如全局调试埋点) |
| 2. 目标 | 命中元素本身 | — | — |
| 3. 冒泡 | 目标的父 → ... → window | false(默认) | 日常监听、事件委托 |
true。五、事件委托:性能利器
冒泡机制的最大杀手锏是事件委托:只给祖先绑一个监听器,
利用冒泡捕获所有后代的事件,再用 event.target 判断「实际点的是谁」。
它有两个核心收益:① 省监听器(100 个 li 不必绑 100 个);②
支持动态元素(后加的 li 不用重新绑)。
- 苹果
- 香蕉
- 橘子
- 葡萄
- 西瓜
六、this 与箭头函数
普通函数 handler 里,this === event.currentTarget(绑监听的元素);
但箭头函数没有自己的 this,它会沿用「定义时外层」的 this。
选哪个写法要看场景:需要 this 就用普通函数;只需要 event.target 就用箭头,写法更短。
this = currentTarget);② 箭头函数(this = 外层的 window)。点按钮分别看 this 到底是谁this 的方法 → 普通函数(比如 Vue 2 的 methods、
React 的 class component 写法)。新版 React Hooks 用箭头也行了,所以箭头越用越多。七、自定义事件 CustomEvent
除了浏览器原生事件,你也可以自己"造"一个事件——
用 new CustomEvent(name, { detail }) 创建,
用 el.dispatchEvent(event) 派发,
用 addEventListener 在任何地方监听。
它最常见的舞台是组件间通信 / 跨模块解耦
——A 模块只管 dispatch,B 模块负责 listen,相互不直接 import。
'msg:hello' 自定义事件,另一处监听并取出 detail。你也可以加一个监听 'msg:goodbye',按钮 dispatch 的就是它document(最大、最稳),或者某棵树的根节点。
注意 event.detail 在监听者拿不到 this 时仍可读,所以
即使把 handler 写成箭头函数也没问题。八、本章小结
addEventListener(type, handler, options)是事件核心 API; 三参数依次为事件名、回调函数、监听选项({once, capture, passive})。event.target是实际触发的元素;event.currentTarget(与this同)是绑监听的元素——事件委托靠target判断真实目标。- 事件流三阶段:捕获 → 目标 → 冒泡;
默认
false在冒泡阶段触发,true是捕获。 stopPropagation()阻止冒泡;preventDefault()阻止默认行为——互不相干。- 事件委托:把监听绑到祖先,用
target.closest()命中真实元素,省监听 / 支持动态元素。 - 普通函数 handler 的
this === currentTarget;箭头函数无自己的this,沿用外层。 new CustomEvent(name, {detail})+dispatchEvent+addEventListener实现跨模块通信雏形(组件解耦利器)。- 同一事件可绑多个 listener、相互不覆盖;用
removeEventListener解绑时必须传入同一个函数引用。
addEventListener 给三个按钮各绑 click,把点击结果打印到 #log;
② 写一个 <ul> 含 8 个 <li>,在外层 ul 上只绑一次委托 handler,
点击时高亮当前 li 并打印它;
③ 写一个 "component:save" 自定义事件,详情带 { id, title },
父节点 dispatch,子组件监听并 console.log 出 detail。
完成后尝试把列表改用委托、新增 li 也能响应,把子组件换成箭头函数 handler,
体会"this 与 target 各管各"的设计意图。