全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2025-08-23_不要再用 removeEventListener 了!这个API救了我的命

您的位置:首页 >> 新闻 >> 行业资讯

不要再用 removeEventListener 了!这个API救了我的命 (??金石瓜分计划火热进行中,速戳上图了解详情??) 不要再用 removeEventListener 了!这个 API 救了我的命昨天被产品经理叫到办公室,说用户反馈我们的后台管理系统越用越卡,Chrome 任务管理器显示内存占用已经飙到 2GB 了。我 tm 当场就懵了,这不是在打我脸吗? 回到工位一番排查,发现罪魁祸首竟然是那些没清理干净的事件监听器。看着满屏的addEventListener和对应的清理代码,我突然想起了之前看到过但一直没用的AbortController。 试了一下,卧槽,真香。 先看看我写的这坨屎// 我之前写的"杰作",现在看着都想删库跑路export defaultclassDataGrid{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 defaultclassDataGrid{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 也不是一帆风顺的。记得刚开始用的时候,我直接这样写: // 错误示范,别学我classModal{show() { this.controller=newAbortController(); 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 就废了,不能重复使用。 正确的写法应该是: classModal{constructor() { this.controller=newAbortController(); } 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=newAbortController(); }}真实项目:拖拽排序的坑前段时间做一个看板功能,需要实现卡片拖拽排序。用传统方式写的话,光是事件监听器的管理就能把人逼疯: classDragSort{constructor(container) { this.container= container; this.isDragging=false; this.dragElement=null; this.initDrag(); } initDrag() { constdragController =newAbortController(); this.dragController= dragController; const{ signal } = dragController; // 只在容器上监听mousedown this.container.addEventListener('mousedown',(e) ={ constcard = e.target.closest('.card'); if(!card)return; this.startDrag(card, e); }, { signal }); } startDrag(card, startEvent) { // 为每次拖拽创建独立的controller constmoveController =newAbortController(); const{ signal } = moveController; this.isDragging=true; this.dragElement= card; conststartX = startEvent.clientX; conststartY = startEvent.clientY; constrect = card.getBoundingClientRect(); // 创建拖拽副本 constghost = 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) ={ constdeltaX = e.clientX- startX; constdeltaY = 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'; functionuseEventController() {constcontrollerRef =useRef(); useEffect(() ={ controllerRef.current=newAbortController(); return() ={ controllerRef.current?.abort(); }, []); constaddEventListener= (target, event, handler, options = {}) = { if(!controllerRef.current)return; constelement = target?.current|| target; if(!element)return; element.addEventListener(event, handler, { signal: controllerRef.current.signal, ...options return{ addEventListener };} // 使用起来贼爽functionMyComponent() {const{ addEventListener } =useEventController();constbuttonRef =useRef(); useEffect(() ={ addEventListener(window,'resize',(e) ={ console.log('窗口大小变了'); addEventListener(buttonRef,'click',(e) ={ console.log('按钮被点了'); }, []); returnbuttonref={buttonRef}点我/button;}兼容性和实际使用建议AbortController 在主流浏览器中支持得还不错,Chrome 66+、Firefox 57+、Safari 11.1 + 都能用。我们项目的用户主要是企业客户,浏览器版本都比较新,所以直接用了。 如果你需要兼容老浏览器,可以加个简单的判断: classEventManager{constructor() { this.useAbortController ='AbortController'inwindow; 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 绝对是更好的选择。 如果你也经常被事件监听器的管理搞得头疼,试试这个方法吧。保证你用了就回不去了。 ?写这篇文章的时候,我又想起了那个 2GB 内存占用的 bug。现在想想,要是早点用 AbortController,也不至于被产品经理叫到办公室 "喝茶" 了。?? ?AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding 点击"阅读原文"了解详情~ 阅读原文

上一篇:2023-08-07_中央科技部门决算汇总公布,中科院2022年收入1721.19亿 下一篇:2023-07-13_看完上半年的AIGC案例 , 觉得自己失不了业

TAG标签:

19
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价