稀土掘金技术社区 09月11日
别再用 removeEventListener 了!AbortController 救了我的命
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

后台管理系统内存占用飙到 2GB,问题竟出在没清理干净的事件监听器上。作者分享使用 AbortController 管理事件监听器的经验,包括在 DataGrid、拖拽排序和 React 项目中的应用,以及兼容性和使用建议。

💡 使用 AbortController 管理事件监听器,可以避免忘记清理导致的内存泄漏问题。在 DataGrid 类中,通过将所有事件监听器绑定到同一个 AbortController,只需一行代码即可清理所有事件监听器。

🚀 在拖拽排序功能中,为每次拖拽创建独立的 AbortController,确保拖拽结束后自动清理相关事件,避免事件监听器累积导致的 bug。

✅ 在 React 项目中,封装 useEventController hook 简化事件监听器的管理,通过 React 的 useEffect 钩子自动清理事件监听器。

👍 AbortController 在主流浏览器中支持良好,但需要考虑兼容老浏览器的情况,可以降级到传统的事件监听器管理方式。

🔧 使用 AbortController 可以使代码更简洁,bug 更少,维护成本降低,是管理事件监听器的更好选择。

原创 四月友人A 2025-08-23 09:01 重庆

昨天被产品经理叫到办公室,说用户反馈我们的后台管理系统越用越卡,Chrome 任务管理器显示内存占用已经飙到 2GB 了。我 tm 当场就懵了,这不是在打我脸吗?

(💰金石瓜分计划火热进行中,速戳上图了解详情🔍)

不要再用 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.overlaythis.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, oncetrue });

                // 防止文本选中
                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.currentreturn;

                  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 绝对是更好的选择。

                如果你也经常被事件监听器的管理搞得头疼,试试这个方法吧。保证你用了就回不去了。

                写这篇文章的时候,我又想起了那个 2GB 内存占用的 bug。现在想想,要是早点用 AbortController,也不至于被产品经理叫到办公室 "喝茶" 了。😅

                AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding

                ""~

                阅读原文

                跳转微信打开

                Fish AI Reader

                Fish AI Reader

                AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

                FishAI

                FishAI

                鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

                联系邮箱 441953276@qq.com

                相关标签

                AbortController 事件监听器 内存泄漏 前端开发 React
                相关文章