B09 事件对象与自定义事件

2026-10-31 17:110 阅读约 13 分钟JavaScript

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 验证与讨论

  1. 点击「删除」按钮,确认 target 为 BUTTON、currentTarget 为 list,且能取到 data-id。
  2. 点击外链,确认页面不跳转;按住修饰键再点,确认恢复跳转。
  3. 连续点击两次,确认 once 回调只执行一次。
  4. 在控制台执行 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

0/2000

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