B09 事件对象与自定义事件
B09 事件对象与自定义事件
摘要:一次事件在 DOM 树中经历捕获、目标、冒泡三个阶段,监听器默认在冒泡阶段触发。事件对象同时携带
target(真实来源)与currentTarget(当前监听器所在元素),二者在委托场景下不同。CustomEvent加dispatchEvent让组件在不暴露内部实现的前提下对外发消息。本文给出三阶段模型、事件对象字段、五个监听器选项与自定义事件的两类用法。
关键词:事件流,捕获,冒泡,事件委托,CustomEvent
所属:frontend/js / B-进阶 | 上一篇:B08-正则表达式 | 下一篇:C00-总结 | 返回:B00-总结
1 引言
事件委托里用 event.target 取元素时,点击子元素拿到的是子元素;同一份数据要传给多个模块时,常见做法是直接调用彼此的函数,形成双向依赖。
本文给出事件传播模型与自定义事件两种解法。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 传播 | 三阶段、捕获与冒泡、委托取值 | 合成事件系统(D01) |
| 事件对象 | 常用字段、默认行为、传播控制 | 指针与触摸事件细节 |
| 自定义 | CustomEvent、dispatchEvent |
事件总线框架 |
| 选项 | once、passive、capture、signal |
滚动性能调优(CSS C06) |
表 1-1 本文覆盖范围
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Chrome 130+,本地服务器 | 页面经 http 打开 |
| 前置 | A08 | 会事件委托与解绑 |
| 文件 | js-practice/events.html、events.mjs |
编码 UTF-8 |
表 2-1 环境与前置要求
3 原理与实现
3.1 三阶段传播
事件先从根向下捕获,到达目标后向上冒泡,监听器按注册阶段被触发,如图 3-1 所示。
flowchart TD
A["window"] --> B["document"]
B --> C["ul 捕获阶段"]
C --> D["li 目标阶段"]
D --> E["ul 冒泡阶段 默认在此触发"]
E --> F["document"]
F --> G["window"]
D --> H["target 恒为真实来源"]
E --> I["currentTarget 为当前监听元素"]
图 3-1 事件的捕获、目标与冒泡三段路径
图上可见:委托监听器在冒泡阶段被触发,此时
target可能是任意后代,currentTarget才是绑监听器的那个元素。
3.2 完整实现
<!-- js-practice/events.html;运行环境:Chrome 130+ -->
<ul id="list">
<li data-id="1"><button type="button">删除</button>第一条</li>
<li data-id="2"><button type="button">删除</button>第二条</li>
</ul>
<a id="link" href="https://example.com">外链</a>
// js-practice/events.mjs;运行环境:Chrome 130+
const list = document.querySelector('#list');
const link = document.querySelector('#link');
// 1 委托:target 是真实来源,currentTarget 是监听元素
list.addEventListener('click', (event) => {
const row = event.target.closest('li');
console.log('target', event.target.tagName); // BUTTON 或 LI
console.log('currentTarget', event.currentTarget.id); // list
console.log('row id', row?.dataset.id);
});
// 2 捕获阶段监听:在到达目标前拦截
link.addEventListener(
'click',
(event) => {
if (!event.metaKey) event.preventDefault(); // 取消默认跳转
},
{ capture: true },
);
// 3 阻断传播:只影响后续阶段,不取消默认行为
list.addEventListener('click', (event) => {
if (event.target.dataset.silent === 'true') event.stopPropagation();
});
// 4 一次性与被动监听器
const once = () => console.log('只触发一次');
list.addEventListener('click', once, { once: true });
window.addEventListener('scroll', () => {}, { passive: true }); // 不调用 preventDefault,可并行滚动
// 5 自定义事件:组件对外发消息,不暴露内部方法
function createCounter() {
const bus = new EventTarget();
return {
onChange: (fn) => bus.addEventListener('change', fn),
inc() {
bus.dispatchEvent(new CustomEvent('change', { detail: { at: Date.now() } }));
},
};
}
const counter = createCounter();
counter.onChange((event) => console.log('changed at', event.detail.at));
counter.inc();
// 6 在 DOM 元素上派发并冒泡
list.dispatchEvent(
new CustomEvent('item:removed', {
bubbles: true, // 允许冒泡到祖先
detail: { id: 2 },
}),
);
list.addEventListener('item:removed', (event) => console.log('removed', event.detail.id));
事件对象常用字段见表 3-1。
| 字段 | 含义 | 委托中的取值 |
|---|---|---|
type |
事件类型字符串 | 与监听器类型一致 |
target |
事件的真实来源元素 | 可能是任意后代 |
currentTarget |
当前执行监听器所在元素 | 恒为绑监听器的元素 |
eventPhase |
阶段编号 1 捕获 / 2 目标 / 3 冒泡 | 用于判断触发位置 |
bubbles |
是否冒泡 | 自定义事件默认 false |
detail |
CustomEvent 携带的数据 |
任意结构 |
defaultPrevented |
默认行为是否已被取消 | 用于判断是否继续处理 |
表 3-1 事件对象字段
监听器选项见表 3-2。
| 选项 | 作用 | 典型用途 |
|---|---|---|
capture |
在捕获阶段触发 | 拦截、预处理 |
once |
触发一次后自动解绑 | 初始化、一次性确认 |
passive |
声明不会调用 preventDefault |
滚动与触摸,避免阻塞 |
signal |
随 AbortController 解绑 |
组件销毁时统一清理(D01) |
表 3-2 四个监听器选项
4 验证与讨论
- 点击「删除」按钮,确认
target为BUTTON、currentTarget为list,且能取到data-id。 - 点击外链,确认页面不跳转;按住修饰键再点,确认恢复跳转。
- 连续点击两次,确认
once回调只执行一次。 - 在控制台执行
list.dispatchEvent(new CustomEvent('item:removed', {bubbles:true, detail:{id:9}})),确认祖先监听器也能收到。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 委托取值 | closest 能回溯到行元素 |
表 3-1 |
| 默认行为 | preventDefault 阻止跳转 |
3.2 节 |
| 阶段差异 | 捕获早于目标、目标早于冒泡 | 图 3-1 |
| 自定义事件 | detail 可被监听器读出 |
表 3-1 |
| 冒泡开关 | bubbles: false 时祖先收不到 |
表 3-1 |
表 4-1 五项验收
讨论与局限。 stopPropagation 只阻断后续阶段,不取消默认行为;需要同时阻止时用 stopImmediatePropagation(还会阻止同元素上的其余监听器)。另,自定义事件名建议加命名空间前缀(item:removed),避免与原生事件或第三方库重名。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 委托里取到子元素 | 用了 event.target |
用 closest 回溯,或读 currentTarget |
preventDefault 无效 |
监听器注册在 passive 模式下 |
去掉 passive |
| 祖先收不到自定义事件 | 未设 bubbles: true |
派发时显式开启冒泡 |
| 同一点击触发两次 | 冒泡路径上绑了两个监听器 | 明确委托层级,只在一个容器上绑定 |
| 解绑后仍触发 | 用了匿名函数 | 提取命名函数或用 signal |
| 滚动监听导致卡顿 | 未用 passive |
滚动与触摸一律加 passive: true |
表 5-1 事件相关的六类问题
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 委托中用 closest 从 target 回溯 |
图 3-1 |
| 2 | currentTarget 才是监听器所在元素 |
表 3-1 |
| 3 | 滚动与触摸监听器加 passive |
表 3-2 |
| 4 | 组件对外通信用 CustomEvent,不暴露方法 |
3.2 节 |
表 6-1 结论汇总
参考文献
[1] MDN Web Docs · 事件冒泡与捕获,https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events(访问于 2026-10-31)
[2] DOM Standard · Event dispatch,WHATWG,https://dom.spec.whatwg.org/#event-dispatch(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~