JS 事件系统完全指南与性能优化

在 JavaScript 的世界里,事件就像网页的 “交互信号”—— 用户点击按钮、滚动页面、输入文字,都是通过事件让网页做出反应。今天我们从基础到进阶,把浏览器事件机制拆解开,再聊聊怎么用得更高效,避免常见的性能坑!

一、事件系统:网页的 “交互逻辑骨架”

1.1 事件流三阶段:从顶层到目标,再回到顶层

事件触发后,不是直接定位到目标元素,而是会经历 “捕获 → 目标 → 冒泡” 三个阶段,就像水流先向下流到目标,再向上回流:

  • 捕获阶段 (Capture Phase):事件从 window 开始,顺着 DOM 树向下传递,直到目标元素的父级(比如点击按钮时,先经过 bodydiv,再到按钮的父元素)
  • 目标阶段 (Target Phase):事件终于到达触发的目标元素(比如刚才的按钮)
  • 冒泡阶段 (Bubble Phase):事件从目标元素开始,顺着 DOM 树向上 “回流”,回到 window(按钮 → 父 divbodywindow

1.2 三种事件监听方式:哪种最实用?

日常开发中,绑定事件主要有三种方式,各有优劣,我整理成了表格:

方式 示例 👍 优点 👎 缺点
HTML 属性绑定 <button onclick="handleClick()"> 写起来快,适合简单 demo HTML 和 JS 混在一起,代码难维护;无法绑定多个处理函数
DOM 属性绑定 btn.onclick = handleClick 比 HTML 属性清晰,不用混写 一个事件只能绑一个函数,后面的会覆盖前面的
addEventListener btn.addEventListener('click', handleClick) 🏆 推荐!支持绑多个函数;能控制事件阶段;可手动移除 需要手动解绑,否则可能内存泄漏

实际开发里,我几乎只用 addEventListener—— 它的灵活性和控制力是另外两种方式比不了的,尤其是复杂项目里,多监听器、阶段控制这些功能很关键。

1.3 addEventListener详解:参数怎么用?

基本语法

它有三种调用形式,核心是控制事件的触发规则:

1
2
3
4
5
6
7
8
// 1. 基础版:事件类型 + 处理函数
addEventListener(type, listener);

// 2. 旧版阶段控制:加个布尔值控制捕获/冒泡
addEventListener(type, listener, useCapture);

// 3. 新版选项控制:更灵活的配置(推荐)
addEventListener(type, listener, options);

关键参数说明

  • useCapture(布尔值,旧版)
    控制事件在哪个阶段触发。true 是捕获阶段,false(默认)是冒泡阶段,和新版 options.capture 功能完全一样。

  • options(对象,新版)
    能精细控制事件行为,常用选项如下:

    选项 类型 默认值 作用
    capture Boolean false 决定事件在哪个阶段触发:false(冒泡阶段)、true(捕获阶段)
    once Boolean false true 时,函数只执行一次,执行完自动解绑(比如 “点击后失效” 的按钮)
    passive Boolean false true 时,告诉浏览器 “这个函数不会调用 preventDefault()”,能提升滚动、触摸等高频事件的性能
    signal AbortSignal - 配合 AbortController 使用,调用 abort() 就能批量解绑事件

二、常用事件类型:按场景分类整理

浏览器事件类型非常多,我附上关键属性和用法,不用死记,需要时查就行。

先看事件的继承关系

所有事件都基于 Event 这个 “基类”,再衍生出不同场景的事件,比如鼠标事件、键盘事件等,关系如下:

1. Event(基础事件)

所有事件的 “祖宗”,提供最基础的属性和方法,比如判断事件是否冒泡、阻止默认行为等。

常用属性

  • type:事件类型(如 “click”)
  • target:实际触发事件的元素(比如点击按钮,target 就是按钮)
  • currentTarget:始终为绑定事件的元素(比如给父容器绑事件,点击子元素时,currentTarget 是父容器)
  • bubbles:布尔值,判断事件是否会冒泡
  • cancelable:布尔值,判断事件能否用 preventDefault() 阻止默认行为
  • eventPhase:事件当前阶段(0 = NONE, 1 = CAPTURING, 2 = AT_TARGET, 3 = BUBBLING)
  • timeStamp:事件发生的时间戳
  • isTrusted:事件是否由用户触发(true)还是脚本创建(false)
  • defaultPrevented:是否已阻止默认行为

常用方法

  • preventDefault():阻止默认行为(比如阻止表单提交、链接跳转),可以使用Event.cancelable来检查该事件是否支持取消
  • stopPropagation():阻止事件继续冒泡或捕获(比如点击子元素后,父元素的事件不触发)
  • stopImmediatePropagation():阻止其他监听器执行

高频基础事件

事件 触发时机 常用场景
DOMContentLoaded DOM 解析完成(不用等图片、CSS 加载) 页面初始化逻辑(比如绑定事件、渲染列表)
load 整个页面(图片、CSS 等)加载完成 处理图片相关逻辑(比如获取图片尺寸)
scroll 元素或页面滚动时 滚动加载、导航栏吸顶
resize 窗口或元素大小改变时 响应式布局调整(比如窗口缩小后重排内容)
submit 表单点击提交按钮或按回车时 表单验证、阻止默认提交后用 AJAX 提交

2. UIEvent(用户界面事件)

继承自 Event,处理与浏览器 UI 相关的事件。

特有属性:

  • view:关联的窗口对象
  • detail:事件详情(如点击次数)

2.1 MouseEvent(鼠标事件)

继承自 UIEvent,处理所有鼠标相关事件,比如点击、移动、滚轮操作都属于这类。

特有属性:

  • screenX, screenY:屏幕坐标
  • clientX, clientY:视口坐标
  • pageX, pageY:文档坐标
  • screenX, screenY:屏幕坐标
  • offsetX, offsetY:目标元素坐标
  • button:按下的鼠标按钮(0 = 左键, 1 = 中键, 2 = 右键)
  • buttons:按下的多个按钮
  • relatedTarget:相关元素(如 mouseover 时的来源元素)

具体事件类型:

事件类型 触发时机 是否冒泡 注意点
click 鼠标按下并释放(通常是左键) 双击会触发两次 click
dblclick 双击鼠标 响应速度比 click 慢,慎用
mousedown/mouseup 鼠标按下 / 释放 区分按下和释放的状态(比如拖拽开始 / 结束)
mousemove 鼠标移动 触发频率高,需要节流优化
mouseenter/mouseleave 鼠标进入 / 离开元素 不冒泡,子元素不会触发(比如鼠标从父元素进子元素,不会触发父元素的 mouseleave
mouseover/mouseout 鼠标进入 / 离开元素或子元素 冒泡,子元素会触发(比如鼠标从父元素进子元素,父元素会触发 mouseout
contextmenu 右键菜单
2.1.1 WheelEvent(滚轮事件)

继承自 MouseEvent,处理滚轮 / 触控板滚动事件。

特有属性:

  • deltaX:水平滚动量(像素)
  • deltaY:垂直滚动量(像素)
  • deltaZ:Z 轴滚动量(3D 设备)
  • deltaMode:滚动单位(0 = 像素, 1 = 行, 2 = 页)

具体事件类型:

事件类型 触发时机 注意点
wheel 滚轮滚动 控制滚动方向和速度(替代旧的 mousewheel 事件)
2.1.2 DragEvent(拖放事件)

继承自 MouseEvent,处理拖放事件。

特有属性:

  • dataTransfer:在拖放交互期间传输的数据

具体事件类型:

事件类型 触发时机
drag 拖动元素(含选择的文本,下同)
dragstart, dragend 开始拖动元素, 拖动操作结束(释放鼠标按钮或按下退出键)
dragover 元素拖动到有效放置目标上(每几百毫秒)
dragenter, dragleave 拖动的元素进入放置目标, 拖动的元素离开放置目标
drop 在放置目标上放置元素
2.1.3 PointerEvent(指针事件)

继承自 MouseEvent,统一鼠标、触摸、触控笔事件。

特有属性:

  • pointerId:唯一指针 ID
  • width, height:接触区域尺寸
  • pressure:压力值(0-1)
  • pointerType:设备类型(” mouse”, “pen”, “touch”)
  • isPrimary:是否为主指针

具体事件类型:

事件类型 触发时机
pointerdown, pointerup 指针按下, 指针释放
pointermove 指针移动
pointerover, pointerout 指针进入元素, 指针离开元素
pointerenter, pointerleave 指针进入元素, 指针离开元素
pointercancel 指针中断
gotpointercapture, lostpointercapture 元素启用捕获后触发, 捕获被释放后触发

2.2 KeyboardEvent(键盘事件)

继承自 UIEvent,处理键盘输入事件,比如快捷键、输入验证等。

特有属性:

  • key:按键的实际内容(比如按 “A” 是 "a""A",按回车是 "Enter"
  • code:按键的物理位置(比如按 “A” 是 "KeyA",不管是否按 Shift,位置不变)
  • location:按键位置(0 = 标准, 1 = 左侧, 2 = 右侧, 3 = 数字键盘)
  • repeat:布尔值,判断是否是长按重复触发
  • isComposing:布尔值,判断是否在输入法输入中(比如中文输入时避免误触发)
  • 修饰键状态:altKey, ctrlKey, shiftKey, metaKey

方法:

  • getModifierState(key):检查特定修饰键状态

具体事件类型:

事件 触发时机 是否冒泡 常用场景
keydown 按下任意键(包括功能键) 监听快捷键(比如 Ctrl + S 保存)
keyup 释放按键时 取消快捷键状态(比如松开 Ctrl 后停止批量操作)

注意:keypress 事件已过时,尽量用 keydown 替代,它支持所有按键类型。

2.3 FocusEvent(焦点事件)

继承自 UIEvent,处理元素焦点变化,比如表单输入时的交互。

特有属性:

  • relatedTarget:相关元素(如失去焦点时获得焦点的元素)

具体事件类型:

事件类型 触发时机 是否冒泡 注意点
focus/blur 元素获得 / 失去焦点 不冒泡,无法用事件委托
focusin/focusout 元素获得 / 失去焦点 冒泡,推荐用它做事件委托(比如表单所有输入框的焦点处理)

2.4 TouchEvent(触摸事件)

继承自 UIEvent,处理触摸屏设备交互。

特有属性:

  • touches:当前所有触摸点
  • targetTouches:当前元素上的触摸点
  • changedTouches:本次事件相关的触摸点

具体事件类型:

事件类型 触发时机
touchstart 触摸开始
touchmove 触摸移动
touchend 触摸结束
touchcancel 触摸中断

2.5 InputEvent(输入事件)

继承自 UIEvent,处理用户输入事件,比如实时监听输入框、文本域的内容变化,比 change 事件更灵敏(change 需要失去焦点才触发)。

特有属性:

  • data:插入的字符
  • dataTransfer:拖放或插入 / 删除的数据
  • inputType:更改的类型(insertingdeleting

具体事件类型:

事件类型 触发时机 常用场景
input 用户输入 实时搜索提示、输入字数统计

3. AnimationEvent(动画事件)

继承自 Event,处理 CSS 动画相关事件。

特有属性:

  • animationName:动画名称
  • elapsedTime:动画已运行时间(秒)
  • pseudoElement:关联的伪元素(如 “::before”)

具体事件类型:

事件类型 触发时机
animationstart, animationend 动画开始, 动画结束
animationiteration 动画重复播放
animationcancel 动画被取消

4. TransitionEvent(过渡事件)

继承自 Event,处理 CSS 过渡效果相关事件。

特有属性:

  • propertyName:发生过渡的 CSS 属性
  • elapsedTime:过渡已运行时间(秒)
  • pseudoElement:关联的伪元素

具体事件类型:

事件类型 触发时机
transitionrun 过渡创建时
transitionstart 过渡实际开始时
transitionend 过渡完成时
transitioncancel 过渡被取消时

5. ClipboardEvent(剪贴板事件)

继承自 Event,用于处理剪切板相关信息的事件。

特有属性:

  • clipboardData:受剪贴板操作影响的数据

具体事件类型:

事件类型 触发时机
copy 复制元素的内容
cut 剪切元素的内容
paste 将内容粘贴到元素中

6. CustomEvent(自定义事件)

继承自 Event,用于创建自定义事件。

特有属性:

  • detail:自定义数据

方法:

  • initCustomEvent():初始化自定义事件

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 创建自定义事件:事件名 + 配置(可传自定义数据)
const dataLoadedEvent = new CustomEvent('data-loaded', {
detail: { list: [1, 2, 3] }, // 自定义数据,通过 event.detail 访问
bubbles: true, // 是否冒泡
cancelable: true, // 是否可阻止默认行为
});

// 2. 绑定事件监听
document.addEventListener('data-loaded', (event) => {
console.log('收到数据:', event.detail.list); // 输出 [1,2,3]
});

// 3. 触发事件
document.dispatchEvent(dataLoadedEvent);

三、事件性能优化:避免卡顿、减少浪费

事件处理很容易出性能问题,比如滚动、拖拽这类高频事件,处理不好会让页面卡顿。分享几个我实战中验证过的优化技巧:

3.1 高频事件:用防抖(debounce)和节流(throttle)控频率

scrollmousemoveresize 这类事件,触发频率非常高(比如滚动时每秒触发几十次),直接执行处理函数会占用大量主线程,导致页面卡顿。这时候需要用 “防抖” 或 “节流” 控制执行次数。

防抖(debounce):连续触发只执行最后一次

比如窗口 resize 时,用户拖动窗口的过程中不执行,等用户停手后再执行一次,避免频繁计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 防抖:最后的胜利者(连续触发只执行最后一次)
function debounce(fn, delay) {
let timer; // 用闭包保存定时器
return function (...args) {
clearTimeout(timer); // 每次触发都清空之前的定时器
// 重新计时,等 delay 毫秒后执行
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

// 用法:窗口 resize 时,等 200ms 稳定后再执行
window.addEventListener(
'resize',
debounce(() => {
console.log('窗口大小稳定了,执行调整逻辑');
}, 200),
);

节流(throttle):固定间隔执行一次

比如滚动加载时,不管滚动多快,每隔 100ms 只执行一次,避免请求发送太频繁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function throttle(fn, interval) {
let lastTime = 0; // 上次执行时间
return function (...args) {
const now = Date.now();
// 距离上次执行超过 interval 才执行
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now; // 更新上次执行时间
}
};
}
// 用法:滚动时每隔 100ms 执行一次
document.addEventListener(
'scroll',
throttle(() => {
console.log('滚动中,按固定间隔执行');
}, 100),
);

3.2 多元素事件:用事件委托减少监听器

如果有很多相同元素需要绑定事件(比如列表里的删除按钮),一个个绑定会创建大量监听器,浪费内存。这时候用 “事件委托”,把监听器绑在父元素上,通过 event.target 判断是否点击目标元素。

反面示例:逐个绑定(低效)

1
2
3
4
5
6
// 给每个 .remove-btn 绑事件,元素多的时候很卡
document.querySelectorAll('.remove-btn').forEach((btn) => {
btn.addEventListener('click', () => {
btn.parentElement.remove();
});
});

正面示例:事件委托(高效)

1
2
3
4
5
6
7
// 只给父容器绑一个事件,不管有多少子元素都能处理
document.getElementById('list-container').addEventListener('click', (event) => {
// 判断点击的是不是 .remove-btn
if (event.target.matches('.remove-btn')) {
event.target.parentElement.remove();
}
});

额外优势:支持动态元素

如果列表是动态生成的(比如 AJAX 加载后新增的项),逐个绑定的事件会失效,但事件委托能自动处理 —— 因为监听器在父容器上,新元素只要符合选择器,点击就能触发。

性能秘诀:动态内容使用事件委托是高性能 Web 应用的关键!

3.3 一次性事件:用 once: true 自动解绑

有些事件只需要执行一次(比如 “点击后弹出提示,之后再点无效”),不用手动解绑,加个 once: true 选项,执行完会自动移除监听器。

1
2
3
4
5
6
7
8
// 可以使用匿名函数,自动在调用后删除,再点不会触发
document.querySelector('.tips-btn').addEventListener(
'click',
() => {
alert('这是只弹一次的提示');
},
{ once: true },
);

3.4 滚动 / 触摸事件:用 passive: true 提升流畅度

浏览器在处理 scrolltouchmove 这类事件时,会先检查函数是否调用 preventDefault(),如果有,会阻塞滚动,导致卡顿。如果你的函数不会阻止默认行为,加 passive: true 告诉浏览器 “不用等检查了,直接执行”,能显著提升滚动流畅度。

1
2
3
4
5
6
7
8
// 滚动事件加 passive: true,提升流畅度
document.addEventListener(
'scroll',
() => {
console.log('滚动中,不阻止默认行为');
},
{ passive: true },
);

注意:如果加了 passive: true,再调用 preventDefault() 会报错,因为浏览器已经认定你不会阻止默认行为了。

3.5 事件顺序控制:用 capture: true 精确控制事件触发顺序(不是常规性能优化的手段)

在某些特定场景下,捕获阶段可能更早地处理事件,从而能够阻止事件进一步传播。

1
2
3
4
5
6
7
8
9
10
document.addEventListener(
'click',
(e) => {
if (e.target.matches('.should-block')) {
e.stopPropagation(); // 阻止所有后续监听器
e.preventDefault();
}
},
{ capture: true }, // 在捕获阶段早期检查
);

3.6 不再需要的事件:用 removeEventListener 及时解绑避免内存泄漏

如果元素被删除(比如单页应用切换组件),但事件监听器没解绑,浏览器会一直持有这个函数和元素的引用,导致内存泄漏。一定要在合适的时机解绑。

正确示例:组件销毁时解绑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ListComponent {
constructor() {
// 绑定this,避免函数内this指向错误
this.handleClick = this.handleClick.bind(this);
this.container = document.getElementById('list-container');
this.container.addEventListener('click', this.handleClick);
}

handleClick(event) {
// 处理点击逻辑
}

// 组件销毁时调用,解绑事件
destroy() {
this.container.removeEventListener('click', this.handleClick);
}
}

// 切换组件时,销毁旧组件
const oldList = new ListComponent();
oldList.destroy(); // 解绑事件,避免内存泄漏

注意点

  1. 匿名函数无法移除,要使用具名函数

  2. 解绑时要和绑定的函数、参数完全一致
    比如绑定的时候用了 capture: true,解绑时也要加,否则解不掉:

    1
    2
    3
    4
    5
    // 绑定:加了 capture: true
    element.addEventListener('click', handleClick, true);

    // 解绑:必须也加 capture: true,否则无效
    element.removeEventListener('click', handleClick, true);

3.7 事件选择原则

  • 优先使用不会冒泡的事件:如 mouseenter/mouseleave 替代 mouseover/mouseout

    这样可以减少事件处理函数被意外触发的可能性,同时也能减少事件传播带来的性能开销(尽管在现代浏览器中这种开销通常很小)。但要注意,它们不能用于事件委托(因为不冒泡),所以使用场景是直接绑定到目标元素。

  • 移动端优先指针事件pointerdown 替代 mousedown/touchstart

    使用pointerdown等事件可以同时支持多种输入方式,避免为鼠标和触摸分别写两套事件逻辑。这有助于代码维护和减少重复。

  • 焦点事件使用冒泡版本focusin/focusout 替代 focus/blur

    因为focusblur事件不冒泡,所以当我们需要在祖先元素上监听焦点变化(例如做表单验证)时,使用冒泡版本的focusinfocusout可以方便地实现事件委托(仅需将事件绑定在表单上),而不必在每个可聚焦元素上单独绑定事件。

四、实战问题:常见坑与解决方案

4.1 滚动事件触发太频繁,页面卡顿

问题scroll 事件每秒触发几十次,处理函数里有 DOM 操作(比如修改样式、计算位置),导致主线程忙不过来,页面卡。
解决方案

  1. 用节流控制执行频率(比如每隔 100ms 执行一次);
  2. 复杂计算用 requestAnimationFrame 包裹,让浏览器在重绘时执行,避免掉帧;
  3. 能用 IntersectionObserver 替代的场景,尽量不用滚动事件(比如 “元素进入视口时加载”)。

优化后的滚动事件处理:

1
2
3
4
5
6
7
8
9
const throttledScroll = throttle(() => {
// 用 requestAnimationFrame 确保在重绘时执行
requestAnimationFrame(() => {
// 处理DOM操作
updateNavPosition();
});
}, 100);

window.addEventListener('scroll', throttledScroll, { passive: true });

不使用滚动事件,而是使用IntersectionObserver

1
2
3
4
5
6
7
8
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 元素进入视口
}
});
});
observer.observe(element);

4.2 移动端点击有 300ms 延迟

问题:早期移动端浏览器为了判断用户是否双击缩放,会在点击后延迟 300ms 再触发 click 事件,导致按钮点击反应慢。

解决方案

  1. 加 viewport meta 标签,禁用缩放(推荐,简单有效);
    1
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
  2. touchstart/touchend 替代 click(需要处理触摸穿透问题);
  3. 旧项目可用 FastClick 库(现在大部分现代浏览器已优化,但兼容旧设备时可能需要)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.6/fastclick.min.js"></script>
    <script>
    if ('addEventListener' in document) {
    document.addEventListener(
    'DOMContentLoaded',
    () => {
    FastClick.attach(document.body);
    },
    false,
    );
    }
    </script>

4.3 事件冒泡导致父元素事件误触发

问题:点击子元素时,父元素的同类型事件也会触发(比如子按钮和父容器都绑了 click 事件)。

解决方案:在子元素的事件处理函数里调用 stopPropagation(),阻止事件继续冒泡。

1
2
3
4
5
6
7
8
9
10
// 子元素事件:阻止冒泡,父元素事件不触发
document.querySelector('.child-btn').addEventListener('click', (event) => {
event.stopPropagation(); // 阻止事件冒泡到父元素
console.log('子元素被点击');
});

// 父元素事件:子元素点击时不会触发
document.querySelector('.parent-container').addEventListener('click', () => {
console.log('父元素被点击');
});

4.4 单页应用切换组件,事件监听器没解绑

问题:单页应用(SPA)切换组件时,旧组件的事件监听器没解绑,导致内存泄漏,甚至出现 “幽灵点击”(切换后旧组件的事件还在触发)。

解决方案

  1. 组件销毁时手动解绑事件(如前面 3.6 的示例);

  2. AbortController 批量解绑(现代浏览器推荐)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 方案2:用 AbortController 批量解绑
    const controller = new AbortController();
    const signal = controller.signal;

    // 绑定多个事件,都用同一个 signal
    element1.addEventListener('click', handleClick1, { signal });
    element2.addEventListener('scroll', handleScroll2, { signal });

    // 组件销毁时,调用 abort() 批量解绑所有事件
    controller.abort();

五、现代事件处理技巧:更高效的玩法

5.1 事件总线(Event Bus):非关联组件通信

如果两个组件没有直接关系(比如兄弟组件、跨层级组件),可以用 “事件总线” 传递消息,不用一层层传 props 或用全局状态。

简单实现事件总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const EventBus = {
// 存储事件:key是事件名,value是回调函数数组
events: {},
// 触发事件:传事件名和数据
emit(eventName, data) {
if (this.events[eventName]) {
// 执行所有绑定的回调
this.events[eventName].forEach((cb) => cb(data));
}
},

// 绑定事件:传事件名和回调
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
},

// 解绑事件(可选)
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter((cb) => cb !== callback);
}
},
};

// 组件A:触发事件
EventBus.emit('user-login', { username: 'test' });

// 组件B:监听事件
EventBus.on('user-login', (userInfo) => {
console.log(`用户${userInfo.username}登录了。`);
});

5.2 AbortController:现代事件管理

前面提到过 AbortController 能批量解绑事件,它还能配合 fetch、定时器等使用,是现代浏览器推荐的 “资源管理” 方案。

用 AbortController 取消 fetch 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const controller = new AbortController();
const signal = controller.signal;

// 发起fetch请求,传signal
fetch('/api/data', { signal })
.then((res) => res.json())
.catch((err) => {
if (err.name === 'AbortError') {
console.log('请求被取消了');
}
});

// 3秒后取消请求(比如用户点击“取消”按钮)
setTimeout(() => controller.abort(), 3000);

最后小测验

当用户点击一个按钮时,事件流的哪个阶段最先触发?
A) 目标阶段
B) 冒泡阶段
C) 捕获阶段

答案:C) 捕获阶段(事件从 window 向下传递到目标元素,先经过捕获阶段,再到目标阶段,最后是冒泡阶段)


事件系统是前端交互的核心,掌握它不仅能写出流畅的交互,还能避免很多隐藏的性能问题。建议大家在实际项目中多尝试优化技巧,比如用事件委托替代多监听器、用 passive 提升滚动流畅度 —— 这些小改动能让页面体验提升一大截!