跟着 MDN 学 HTML day_45:深入理解 EventTarget 接口
📑 目录
| 左列章节 | 右列章节 |
|---|---|
| [一、EventTarget 接口概述](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [二、EventTarget 构造函数](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [三、addEventListener 基本用法](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [四、listener 参数的两种形式](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [五、options 参数详解](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [六、options 兼容性检测](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [七、使用 AbortSignal 移除监听器](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [八、事件监听器中的 this 指向](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [九、与监听器进行数据交换](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [十、removeEventListener 匹配规则](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [十一、dispatchEvent 手动派发事件](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) | [十二、被动事件与性能优化](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
| [十三、内存管理与最佳实践](#左列章节 右列章节 一、EventTarget 接口概述 二、EventTarget 构造函数 三、addEventListener 基本用法 四、listener 参数的两种形式 五、options 参数详解 六、options 兼容性检测 七、使用 AbortSignal 移除监听器 八、事件监听器中的 this 指向 九、与监听器进行数据交换 十、removeEventListener 匹配规则 十一、dispatchEvent 手动派发事件 十二、被动事件与性能优化 十三、内存管理与最佳实践) |
一、EventTarget 接口概述
EventTarget 接口由可以接收事件并且可以创建事件侦听器的对象实现。简而言之,只要一个对象能够成为事件的目标,它就必然实现了与这个接口相关的三个方法:addEventListener、removeEventListener 和 dispatchEvent。
在浏览器环境中,Element、Document 和 Window 是最常见的事件目标。除此之外,XMLHttpRequest、AudioNode、AudioContext 等对象同样可以作为事件目标。
代码示例:验证常见对象是否继承自 EventTarget
javascript
console.log(document instanceof EventTarget); // true
console.log(window instanceof EventTarget); // true
console.log(document.body instanceof EventTarget); // true
许多事件目标还支持通过 onevent 属性或 HTML 特性设置事件处理程序,例如 onclick、onload 等,这是比 addEventListener 更早期的用法,但 addEventListener 提供了更精细的控制能力。
⚠️ 【重点 / 面试考点】
EventTarget是 DOM 事件体系的基石接口,所有能接收事件的对象都实现了它Element、Document、Window是最常见的事件目标,但XMLHttpRequest、AudioNode等也实现了此接口addEventListener相比onXYZ属性具有多监听器 、阶段控制 、移除能力等显著优势
二、EventTarget 构造函数
EventTarget 构造函数用于创建一个新的 EventTarget 对象实例。在实际开发中,显式调用此构造函数的情况极为少见,多数情况下它是在子类的构造函数中通过 super 关键字被间接调用的。
代码示例:直接使用 EventTarget 构造函数
javascript
const myTarget = new EventTarget();
console.log(myTarget instanceof EventTarget); // true
更常见的场景是创建一个继承自 EventTarget 的自定义类,从而让这个类的实例拥有完整的事件能力。
代码示例:自定义事件目标类
javascript
class MyEventTarget extends EventTarget {
constructor(mySecret) {
super();
this._secret = mySecret;
}
get secret() {
return this._secret;
}
}
let myEventTarget = new MyEventTarget(5);
console.log(myEventTarget.secret); // 5
myEventTarget.addEventListener("foo", (e) => {
myEventTarget._secret = e.detail;
});
let event = new CustomEvent("foo", { detail: 7 });
myEventTarget.dispatchEvent(event);
console.log(myEventTarget.secret); // 7
核心结论:这个例子展示了一个完整的事件通信流程------创建自定义事件目标、注册事件监听器、构造并派发自定义事件,最终实现了数据的更新。这种模式在前端组件化架构和状态管理中非常实用。
三、addEventListener 方法的基本用法
addEventListener 是 EventTarget 上最常用的方法,它用于在事件目标上注册特定事件类型的处理程序。其核心语法为 addEventListener(type, listener),也可以通过第三个参数传递 options 对象或 useCapture 布尔值。
代码示例:基本的 click 事件监听
javascript
const button = document.getElementById("myButton");
button.addEventListener("click", function(event) {
console.log("按钮被点击了");
console.log("事件类型:", event.type);
});
与传统的 onXYZ 属性绑定相比,addEventListener 具有显著优势。它允许为同一个事件类型添加多个监听器,这在需要兼容第三方库或模块化代码时尤为重要。
代码示例:同一事件的多个监听器
javascript
button.addEventListener("click", () => console.log("监听器 1"));
button.addEventListener("click", () => console.log("监听器 2"));
button.addEventListener("click", () => console.log("监听器 3"));
// 单次点击将依次输出三条信息
此外,addEventListener 对任何事件类型都有效,而不仅限于 HTML 或 SVG 元素上预定义的事件。
四、listener 参数的两种形式
addEventListener 的 listener 参数可以是一个回调函数 ,也可以是一个实现了 EventListener 接口的对象 ,即拥有 handleEvent 方法的对象。
使用函数作为监听器是最常见的形式:
代码示例:函数作为监听器
javascript
window.addEventListener("resize", function(event) {
console.log("窗口大小发生变化,当前宽度:", window.innerWidth);
});
使用对象作为监听器则提供了更好的组织性和复用性:
代码示例:对象作为监听器(handleEvent)
javascript
const eventHandler = {
handleEvent: function(event) {
switch (event.type) {
case "click":
console.log("处理点击事件");
break;
case "keydown":
console.log("处理键盘事件,键值:", event.key);
break;
}
}
};
document.addEventListener("click", eventHandler);
document.addEventListener("keydown", eventHandler);
核心结论 :
handleEvent方式允许将多个事件的处理逻辑集中在一个对象中,使得代码结构更加清晰,也便于后续的移除操作。
⚠️ 【重点 / 面试考点】
listener参数的两种合法形式:回调函数 或 带有handleEvent方法的对象- 使用对象形式时,事件触发后会自动调用
handleEvent方法,并将事件对象传入 - 对象形式的优势在于多事件复用同一处理器 、便于移除 、逻辑集中管理
五、options 参数详解
addEventListener 的第三个参数从最初的 useCapture 布尔值演变为功能更丰富的 options 对象,支持以下配置项。
代码示例:capture 选项(捕获阶段控制)
javascript
// 捕获阶段触发
parentElement.addEventListener("click", handler, { capture: true });
// 冒泡阶段触发(默认)
parentElement.addEventListener("click", handler, { capture: false });
代码示例:once 选项(单次监听)
javascript
button.addEventListener("click", function() {
console.log("这条信息只会输出一次");
}, { once: true });
代码示例:passive 选项(性能优化声明)
javascript
document.addEventListener("touchstart", handler, { passive: true });
代码示例:signal 选项(AbortController 移除)
javascript
const controller = new AbortController();
element.addEventListener("click", handler, { signal: controller.signal });
// 通过调用 controller.abort() 即可移除该监听器
六、options 支持的浏览器兼容性检测
由于旧版浏览器仍然假定 addEventListener 的第三个参数是布尔值,开发者在使用 options 对象时需要进行特性检测。以下是一个检测 passive 选项是否受支持的经典方案。
代码示例:检测 passive 选项的支持情况
javascript
let passiveSupported = false;
try {
const options = {
get passive() {
passiveSupported = true;
return false;
}
};
window.addEventListener("test", null, options);
window.removeEventListener("test", null, options);
} catch (err) {
passiveSupported = false;
}
console.log("passive 是否受支持:", passiveSupported);
原理解析 :如果浏览器将第三个参数视为对象,就会尝试读取
passive属性,从而触发 getter 函数并将标识设为true。
检测之后可以据此决定如何调用 addEventListener:
javascript
element.addEventListener(
"mouseup",
handleMouseUp,
passiveSupported ? { passive: true } : false
);
七、使用 AbortSignal 移除监听器
AbortSignal 提供了一种优雅地移除事件监听器的方式,特别适用于需要在某些条件满足后自动取消监听的场景。
代码示例:AbortSignal 自动移除监听器
javascript
const controller = new AbortController();
const targetElement = document.getElementById("clickArea");
targetElement.addEventListener("click", function() {
console.log("区域被点击");
// 当满足特定条件时,终止监听
controller.abort();
}, { signal: controller.signal });
核心结论 :在
controller.abort()被调用后,对应的监听器即被移除,无需再手动调用removeEventListener。这在处理复杂的条件逻辑或需要批量移除多个监听器时特别方便。
⚠️ 【重点 / 易错点】
AbortSignal是现代浏览器中替代removeEventListener的推荐方案- 一个
AbortController可以同时控制多个监听器 的移除,只需将它们共用同一个signal controller.abort()调用后,所有使用该signal的监听器都会被一次性移除- 适用于组件卸载、路由切换等需要批量清理事件的场景
八、事件监听器中的 this 指向问题
在使用 addEventListener 注册事件处理器时,处理器函数内部的 this 值默认指向触发事件的元素。
代码示例:普通函数中的 this 指向
javascript
const myElement = document.getElementById("demo");
myElement.addEventListener("click", function(e) {
console.log(this === e.currentTarget); // true
console.log(this.id); // "demo"
});
然而,箭头函数没有自己的 this 上下文 ,它会继承定义时所在作用域的 this。
代码示例:箭头函数中的 this 指向
javascript
myElement.addEventListener("click", (e) => {
console.log(this === e.currentTarget); // false
// 此处的 this 指向外层作用域
});
如果需要在监听器中使用特定的 this 值,可以使用 Function.prototype.bind():
代码示例:使用 bind 绑定自定义 this
javascript
const customThis = { name: "自定义上下文" };
myElement.addEventListener("click", function() {
console.log(this.name); // "自定义上下文"
}.bind(customThis));
在 HTML 属性中直接编写事件处理代码时,this 同样指向当前元素。
html
<button id="myButton" onclick="console.log(this.id)">点击</button>
但如果在属性中调用全局函数,则 this 在非严格模式下指向 window 对象。
html
<script>
function showThis() {
console.log(this); // window(非严格模式)
}
</script>
<button onclick="showThis()">点击</button>
九、与监听器进行数据交换
事件监听器的参数列表只有 event 一个参数,如何才能向监听器传入额外的数据?有三种常见方式。
第一种是使用 bind 方法绑定数据到 this:
代码示例:通过 bind 传入数据
javascript
const data = "传入的数据";
element.addEventListener("click", function() {
console.log(this); // "传入的数据"
}.bind(data));
第二种是利用外部作用域的变量:
代码示例:利用闭包共享数据
javascript
let sharedData = "初始值";
element.addEventListener("click", () => {
console.log(sharedData);
sharedData = "更新后的值";
});
第三种是通过对象引用进行数据交换,由于对象以引用方式存储,多个函数可以共享和修改同一个对象:
代码示例:通过对象引用交换数据
javascript
const dataContainer = { value: "初始" };
element.addEventListener("click", () => {
console.log(dataContainer.value);
dataContainer.value = "已修改";
});
setInterval(() => {
if (dataContainer.value === "已修改") {
console.log("检测到数据变更");
dataContainer.value = "初始";
}
}, 3000);
核心结论 :这三种方式各有适用场景,对象引用的方式最为灵活,因为修改可跨作用域持久化。
十、removeEventListener 方法与匹配规则
removeEventListener 用于移除之前通过 addEventListener 注册的事件监听器。要成功移除,type 和 listener 参数必须完全匹配,而 capture 标志也需要一致。
代码示例:removeEventListener 的基本匹配
javascript
function handleClick(event) {
console.log("处理点击");
}
// 添加监听器(冒泡阶段)
element.addEventListener("click", handleClick, false);
// 成功移除(capture 匹配)
element.removeEventListener("click", handleClick, false);
// 如果添加时使用了捕获阶段
element.addEventListener("click", handleClick, true);
// 则必须用相同的 capture 值移除
element.removeEventListener("click", handleClick, false); // 失败
element.removeEventListener("click", handleClick, true); // 成功
在 options 对象的各个属性中,只有 capture 会影响 removeEventListener 的匹配结果 ,其他属性如 passive、once 等不会影响。
代码示例:options 属性对移除匹配的影响
javascript
element.addEventListener("mousedown", handleMouseDown, { passive: true });
// 以下均成功移除,因为 capture 默认为 false,且 passive 不影响匹配
element.removeEventListener("mousedown", handleMouseDown, { passive: true });
element.removeEventListener("mousedown", handleMouseDown, { passive: false });
element.removeEventListener("mousedown", handleMouseDown, false);
// 以下失败,因为 capture 标志不匹配
element.removeEventListener("mousedown", handleMouseDown, { capture: true });
element.removeEventListener("mousedown", handleMouseDown, true);
⚠️ 【重点 / 面试考点】
removeEventListener成功移除的三个匹配条件 :type一致、listener引用相同、capture一致passive、once、signal不影响移除匹配 ,只有capture起作用- 匿名函数无法被移除,因为无法获取到相同的函数引用
- 推荐使用命名函数 或
AbortSignal来管理监听器生命周期
十一、dispatchEvent 手动派发事件
dispatchEvent 方法允许开发者以编程方式触发事件 ,它会同步 调用所有受影响的 EventListener,并在所有监听器执行完毕后返回。
代码示例:手动派发自定义事件
javascript
const customEvent = new Event("build", { bubbles: true });
element.addEventListener("build", function(e) {
console.log("自定义 build 事件被触发");
});
element.dispatchEvent(customEvent);
关键区别 :与浏览器原生事件通过事件循环异步调用处理程序不同,
dispatchEvent会同步执行 所有监听器。在dispatchEvent返回之前,所有监听器函数都会执行完毕。
dispatchEvent 的返回值是一个布尔值。如果事件可取消(cancelable 为 true)且任意一个监听器调用了 preventDefault,则返回 false;否则返回 true。
代码示例:可取消事件的返回值
javascript
const cancelableEvent = new Event("action", { cancelable: true });
element.addEventListener("action", function(e) {
e.preventDefault();
});
const result = element.dispatchEvent(cancelableEvent);
console.log(result); // false,因为 preventDefault 被调用
自定义事件还可以配合 CustomEvent 传递更丰富的数据:
代码示例:使用 CustomEvent 传递数据
javascript
const detailEvent = new CustomEvent("update", { detail: { id: 42, name: "新数据" } });
element.addEventListener("update", function(e) {
console.log("收到数据:", e.detail);
});
element.dispatchEvent(detailEvent);
十二、被动事件与性能优化
passive 选项对滚动相关性能有着重要影响。根据规范,addEventListener 的 passive 默认值为 false 。然而,在文档级节点的 wheel、mousewheel、touchstart 和 touchmove 等事件上,大部分现代浏览器会将 passive 默认值改为 true,以防止事件监听器阻塞页面滚动。
代码示例:显式声明 passive 为 false
javascript
// 显式声明 passive 为 false 以允许 preventDefault
document.addEventListener("touchstart", function(e) {
e.preventDefault(); // 阻止默认行为
}, { passive: false });
如果 passive 设为 true 但监听器中仍然调用了 preventDefault,浏览器会忽略该调用并输出控制台警告。
代码示例:passive true 时 preventDefault 被忽略
javascript
document.addEventListener("touchstart", function(e) {
e.preventDefault(); // 被忽略,控制台会警告
}, { passive: true });
核心结论 :使用
passive可以显著改善滚屏性能,尤其在移动端,应尽可能将不需要阻止默认行为的事件监听器设为被动。
十三、内存管理与最佳实践
匿名函数作为监听器时,每次调用 addEventListener 都会创建一个新的函数实例,这不仅增加内存开销,还会导致无法通过 removeEventListener 移除该监听器。
代码示例:匿名函数的隐患(不推荐)
javascript
// 不推荐的写法:每次都是新的匿名函数
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", function(e) {
/* 处理点击 */
});
}
代码示例:复用命名函数引用(推荐)
javascript
// 推荐的写法:复用同一个函数引用
function handleClick(e) {
/* 处理点击 */
}
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", handleClick);
}
核心结论 :在移除含有多个监听器的事件目标时,保持对监听器函数的引用是能否成功移除的关键。出于这个原因,模块化和组件化开发中更推荐使用命名的函数引用 ,或者使用
AbortSignal来统一管理监听器的生命周期。
十四、EventTarget 核心方法速查
| 方法 | 功能说明 | 核心参数 | 返回值 |
|---|---|---|---|
addEventListener |
注册事件监听器 | type, listener, options/useCapture |
undefined |
removeEventListener |
移除事件监听器 | type, listener, options/useCapture |
undefined |
dispatchEvent |
手动派发事件 | event(Event 对象) |
boolean |
继承关系示意图
EventTarget
Node
XMLHttpRequest
AudioNode
Window
Element
Document
自定义类
extends EventTarget
✅ 文档总结
EventTarget是 DOM 事件体系的基石接口 ,Element、Document、Window等都继承自它- 构造函数
EventTarget()可以创建独立的事件目标对象,常用于自定义类继承实现组件通信 addEventListener支持多监听器、控制捕获与冒泡阶段、一次性监听、被动监听以及信号移除listener可以是函数 也可以是具有handleEvent方法的对象removeEventListener的匹配严格要求type、listener引用和capture三者一致dispatchEvent可以手动触发事件并同步执行 所有监听器,返回preventDefault是否被调用passive选项对滚动性能优化意义重大,移动端应尽可能设为true- 匿名函数 会带来内存管理和移除上的隐患,推荐使用命名函数或
AbortSignal AbortSignal是现代浏览器中管理监听器生命周期的推荐方案
完整实践示例
javascript
// 创建自定义事件目标
class TaskManager extends EventTarget {
addTask(name) {
this.dispatchEvent(new CustomEvent("taskAdded", {
detail: { taskName: name, timestamp: Date.now() },
bubbles: true
}));
}
}
const manager = new TaskManager();
const controller = new AbortController();
// 注册带 signal 的监听器
manager.addEventListener("taskAdded", (e) => {
console.log(`任务 "${e.detail.taskName}" 已添加`);
}, { signal: controller.signal });
// 触发事件
manager.addTask("学习 EventTarget");
// 需要时统一移除
// controller.abort();
通过动手实践,可以更直观地体会到 EventTarget 在事件驱动编程中的核心地位和强大能力。
想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!