别再费劲 removeEventListener 了!这个冷门 API 让我彻底解脱
haoteby 2025-09-18 19:12 1 浏览
不要再用 removeEventListener了!这个API救了我的命
昨天被产品经理叫到办公室,说用户反馈我们的后台管理系统越用越卡,Chrome任务管理器显示内存占用已经飙到2GB了。我tm当场就懵了,这不是在打我脸吗?
回到工位一番排查,发现罪魁祸首竟然是那些没清理干净的事件监听器。看着满屏的addEventListener和对应的清理代码,我突然想起了之前看到过但一直没用的AbortController。
试了一下,卧槽,真香。
先看看我写的这坨屎
// 我之前写的"杰作",现在看着都想删库跑路
export default class DataGrid {
constructor(container, options) {
this.container = container;
this.options = options;
// 绑定this,一个都不能少,不然就报错
this.handleResize = this.handleResize.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleContextMenu = this.handleContextMenu.bind(this);
this.init();
}
init() {
// 事件监听器注册大会
window.addEventListener('resize', this.handleResize);
this.container.addEventListener('scroll', this.handleScroll);
this.container.addEventListener('click', this.handleClick);
document.addEventListener('keydown', this.handleKeydown);
this.container.addEventListener('contextmenu', this.handleContextMenu);
// 还有定时器要管理
this.resizeTimer = null;
this.scrollTimer = null;
}
destroy() {
// 清理环节,经常漏几个
window.removeEventListener('resize', this.handleResize);
this.container.removeEventListener('scroll', this.handleScroll);
this.container.removeEventListener('click', this.handleClick);
document.removeEventListener('keydown', this.handleKeydown);
// 草,contextmenu忘记清理了
if (this.resizeTimer) clearTimeout(this.resizeTimer);
if (this.scrollTimer) clearTimeout(this.scrollTimer);
}
}
这种写法有多恶心?我来告诉你:
- 写到手酸 - 每个方法都得bind一遍,复制粘贴都嫌烦
- 容易遗漏 - 加了事件监听器,销毁的时候经常忘记清理某几个
- 维护困难 - 想加个新事件?得在两个地方改代码
最要命的是,这个DataGrid会被频繁创建销毁(用户切换页面、筛选数据等),每次忘记清理就是一次内存泄漏。
AbortController拯救了我的职业生涯
export default class DataGrid {
constructor(container, options) {
this.container = container;
this.options = options;
this.controller = new AbortController();
this.init();
}
init() {
const { signal } = this.controller;
// 所有事件监听器统一管理,爽到飞起
window.addEventListener('resize', (e) => {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => this.handleResize(e), 200);
}, { signal });
this.container.addEventListener('scroll', (e) => {
this.handleScroll(e);
}, { signal, passive: true });
this.container.addEventListener('click', (e) => {
this.handleClick(e);
}, { signal });
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' && this.selectedRows.length > 0) {
this.deleteSelectedRows();
}
}, { signal });
this.container.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showContextMenu(e);
}, { signal });
}
destroy() {
// 一行代码解决所有问题!
this.controller.abort();
}
}
你没看错,destroy方法只需要一行代码。当初看到这个效果时,我特么激动得想发朋友圈。
线上踩坑记录
不过用AbortController也不是一帆风顺的。记得刚开始用的时候,我直接这样写:
// 错误示范,别学我
class Modal {
show() {
this.controller = new AbortController();
const { signal } = this.controller;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
}, { signal });
}
hide() {
this.controller.abort();
// 没有重新创建controller!
}
}
结果modal第二次打开的时候,ESC键失效了。原因很简单:controller.abort()之后,这个controller就废了,不能重复使用。
正确的写法应该是:
class Modal {
constructor() {
this.controller = new AbortController();
}
show() {
this.setupEvents();
}
setupEvents() {
const { signal } = this.controller;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
}, { signal });
document.addEventListener('click', (e) => {
if (e.target === this.overlay) this.hide();
}, { signal });
}
hide() {
this.controller.abort();
// 重新创建一个新的controller
this.controller = new AbortController();
}
}
真实项目:拖拽排序的坑
前段时间做一个看板功能,需要实现卡片拖拽排序。用传统方式写的话,光是事件监听器的管理就能把人逼疯:
class DragSort {
constructor(container) {
this.container = container;
this.isDragging = false;
this.dragElement = null;
this.initDrag();
}
initDrag() {
const dragController = new AbortController();
this.dragController = dragController;
const { signal } = dragController;
// 只在容器上监听mousedown
this.container.addEventListener('mousedown', (e) => {
const card = e.target.closest('.card');
if (!card) return;
this.startDrag(card, e);
}, { signal });
}
startDrag(card, startEvent) {
// 为每次拖拽创建独立的controller
const moveController = new AbortController();
const { signal } = moveController;
this.isDragging = true;
this.dragElement = card;
const startX = startEvent.clientX;
const startY = startEvent.clientY;
const rect = card.getBoundingClientRect();
// 创建拖拽副本
const ghost = card.cloneNode(true);
ghost.style.position = 'fixed';
ghost.style.left = rect.left + 'px';
ghost.style.top = rect.top + 'px';
ghost.style.pointerEvents = 'none';
ghost.style.opacity = '0.8';
document.body.appendChild(ghost);
// 拖拽过程中的事件
document.addEventListener('mousemove', (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
ghost.style.left = (rect.left + deltaX) + 'px';
ghost.style.top = (rect.top + deltaY) + 'px';
// 检测插入位置
this.updateDropIndicator(e);
}, { signal });
// 拖拽结束
document.addEventListener('mouseup', (e) => {
this.endDrag(ghost);
// 自动清理本次拖拽的所有事件
moveController.abort();
}, { signal, once: true });
// 防止文本选中
document.addEventListener('selectstart', (e) => {
e.preventDefault();
}, { signal });
// 防止右键菜单
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
}, { signal });
}
destroy() {
this.dragController?.abort();
}
}
这种写法的好处是,每次拖拽开始时创建独立的controller,拖拽结束时自动清理相关事件。不会出现事件监听器累积的问题。
以前用传统方式,我得手动管理mousemove和mouseup的清理,经常出现拖拽结束后事件还在监听的bug。
React项目中的应用
在React项目里,我封装了一个hook:
import { useEffect, useRef } from 'react';
function useEventController() {
const controllerRef = useRef();
useEffect(() => {
controllerRef.current = new AbortController();
return () => {
controllerRef.current?.abort();
};
}, []);
const addEventListener = (target, event, handler, options = {}) => {
if (!controllerRef.current) return;
const element = target?.current || target;
if (!element) return;
element.addEventListener(event, handler, {
signal: controllerRef.current.signal,
...options
});
};
return { addEventListener };
}
// 使用起来贼爽
function MyComponent() {
const { addEventListener } = useEventController();
const buttonRef = useRef();
useEffect(() => {
addEventListener(window, 'resize', (e) => {
console.log('窗口大小变了');
});
addEventListener(buttonRef, 'click', (e) => {
console.log('按钮被点了');
});
}, []);
return <button ref={buttonRef}>点我</button>;
}
兼容性和实际使用建议
AbortController在主流浏览器中支持得还不错,Chrome 66+、Firefox 57+、Safari 11.1+都能用。我们项目的用户主要是企业客户,浏览器版本都比较新,所以直接用了。
如果你需要兼容老浏览器,可以加个简单的判断:
class EventManager {
constructor() {
this.useAbortController = 'AbortController' in window;
if (this.useAbortController) {
this.controller = new AbortController();
} else {
this.handlers = [];
}
}
on(target, event, handler, options = {}) {
if (this.useAbortController) {
target.addEventListener(event, handler, {
signal: this.controller.signal,
...options
});
} else {
// 降级到传统方式
this.handlers.push({ target, event, handler, options });
target.addEventListener(event, handler, options);
}
}
destroy() {
if (this.useAbortController) {
this.controller.abort();
} else {
this.handlers.forEach(({ target, event, handler, options }) => {
target.removeEventListener(event, handler, options);
});
this.handlers = [];
}
}
}
最后
说实话,AbortController这个API我很早就知道,但一直以为只能用来取消fetch请求。直到那次内存泄漏的事故,我才真正开始研究它的其他用法。
现在回头看,这个API真的改变了我写事件处理代码的方式。代码变得更简洁,bug更少,维护成本也大大降低。
当然,不是说传统的addEventListener就一无是处。在某些需要精确控制单个事件监听器的场景下,传统方式可能还是有必要的。但对于大部分日常开发,AbortController绝对是更好的选择。
如果你也经常被事件监听器的管理搞得头疼,试试这个方法吧。保证你用了就回不去了。
ps:写这篇文章的时候,我又想起了那个2GB内存占用的bug。现在想想,要是早点用AbortController,也不至于被产品经理叫到办公室"喝茶"了。
相关推荐
- 如何随时清理浏览器缓存_清理浏览器缓存怎么弄
-
想随时清理浏览器缓存吗?Cookieformac版是Macos上一款浏览器缓存清理工具,所有的浏览器Cookie,本地存储数据,HTML5数据库,FlashCookie,Silverlight,...
- Luminati代理动态IP教程指南配置代理VMLogin中文版反指纹浏览器
-
介绍如何使用在VMLogin中文版设置Luminati代理。首先下载VMLogin中文版反指纹浏览器(https://cn.vmlogin.com)对于刚接触Luminati动态ip的朋友,是不是不懂...
- mac清除工具分享,解除您在安全方面的后顾之忧
-
想要永久的安全的处理掉重要数据,删除是之一,使用今天小编分享的mac清除工具,为您的操作再增一层“保护”,小伙伴慎用哟,一旦使用就不可以恢复咯,来吧一起看看吧~mac清除工具分享,解除您在安全方面的后...
- 取代cookie的网站追踪技术:”帆布指纹识别”
-
【前言】一般情况下,网站或者广告联盟都会非常想要一种技术方式可以在网络上精确定位到每一个个体,这样可以通过收集这些个体的数据,通过分析后更加精准的去推送广告(精准化营销)或其他有针对性的一些活动。Co...
- 辅助上网为啥会被抛弃 曲奇(Cookie)虽甜但有毒
-
近期有个小新闻,大概很多小伙伴都没有注意到,那就是谷歌Chrome浏览器要弃用Cookie了!说到Cookie功能,很多小伙伴大概觉得不怎么熟悉,有可能还不如前一段时间被弃用的Flash“出名”,但它...
- 浏览器指纹是什么?浏览器指纹包括哪些信息
-
本文关键词:浏览器指纹、指纹浏览器、浏览器指纹信息、指纹浏览器原理什么是浏览器指纹?浏览器指纹是指浏览器的各种信息,当我们访问其他网站时,即使是在匿名的模式下,这些信息也可以帮助网站识别我们的身份。...
- 那些通用清除软件不曾注意的秘密_清理不常用的应用软件
-
系统清理就像卫生检查前的大扫除,即使你使出吃奶的劲儿把一切可能的地方都打扫过,还会留下边边角角的遗漏。随着大家电脑安全意识的提高,越来越多的朋友开始关注自己的电脑安全,也知道安装360系列软件来"武装...
- 「网络安全宣传周」这些安全上网小知识你要知道!
-
小布说:互联网改变了人们的衣食住行,但与之伴生的网络安全威胁也不容忽视。近些年来,风靡全球的勒索病毒、时有发生的电信诈骗、防不胜防的个人信息泄露时时刻刻都威胁着我们的生活。9月18日-24日是第四届...
- TypeScript 终极初学者指南_typescript 进阶
-
在过去的几年里TypeScript变得越来越流行,现在许多工作都要求开发人员了解TypeScript...
- jQuery知识一览_jquery的认识和使用
-
一、概览jQuery官网:https://jquery.com/jQuery是一个高效、轻量并且功能丰富的js库。核心在于查询query。...
- 我的第一个Electron应用_electronmy
-
hello,好久不见,最近笔者花了几天时间入门Electron,然后做了一个非常简单的应用,本文就来给各位分享一下过程,Electron大佬请随意~笔者开源了一个Web思维导图,虽然借助showSav...
- HTML5 之拖放(Drag 和 Drop)_html拖放api
-
简介拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在HTML5中,拖放是标准的一部分,任何元素都能够拖放。先点击一个小例子:在用户开始拖动<p>元素时执行JavaScrip...
- 如何用JavaScript判断输入值是数字还是字母?
-
在日常开发中,我们有时候需要判断用户输入的是数字还是字母。本文将介绍如何用JavaScript实现这一功能。检查输入值是否是数字或字母...
- 图形编辑器开发:快捷键的管理_图形编辑工具
-
大家好,我是前端西瓜哥。...
- 浏览器原生剪贴板:原来它能这样读取用户截图!
-
当我们使用GitHub时,会发现Ctrl+V就能直接读取用户剪贴板图片进行粘贴,那么它是如何工作的?安全性如何?...