亚洲乱色熟女一区二区三区丝袜,天堂√中文最新版在线,亚洲精品乱码久久久久久蜜桃图片,香蕉久久久久久av成人,欧美丰满熟妇bbb久久久

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

不要再用 removeEventListener了!這個API救了我的命

freeflydom
2025年8月5日 8:36 本文熱度 1162

昨天被產(chǎn)品經(jīng)理叫到辦公室,說用戶反饋我們的后臺管理系統(tǒng)越用越卡,Chrome任務(wù)管理器顯示內(nèi)存占用已經(jīng)飆到2GB了。我tm當場就懵了,這不是在打我臉嗎?

回到工位一番排查,發(fā)現(xiàn)罪魁禍首竟然是那些沒清理干凈的事件監(jiān)聽器??粗鴿M屏的addEventListener和對應(yīng)的清理代碼,我突然想起了之前看到過但一直沒用的AbortController。

試了一下,臥槽,真香。

先看看我寫的這坨屎

// 我之前寫的"杰作",現(xiàn)在看著都想刪庫跑路
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() {
    // 事件監(jiān)聽器注冊大會
    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() {
    // 清理環(huán)節(jié),經(jīng)常漏幾個
    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);
  }
}

這種寫法有多惡心?我來告訴你:

  1. 寫到手酸 - 每個方法都得bind一遍,復(fù)制粘貼都嫌煩
  2. 容易遺漏 - 加了事件監(jiān)聽器,銷毀的時候經(jīng)常忘記清理某幾個
  3. 維護困難 - 想加個新事件?得在兩個地方改代碼

最要命的是,這個DataGrid會被頻繁創(chuàng)建銷毀(用戶切換頁面、篩選數(shù)據(jù)等),每次忘記清理就是一次內(nèi)存泄漏。

AbortController拯救了我的職業(yè)生涯

export default class DataGrid {
  constructor(container, options) {
    this.container = container;
    this.options = options;
    this.controller = new AbortController();
    
    this.init();
  }
  
  init() {
    const { signal } = this.controller;
    
    // 所有事件監(jiān)聽器統(tǒng)一管理,爽到飛起
    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方法只需要一行代碼。當初看到這個效果時,我特么激動得想發(fā)朋友圈。

線上踩坑記錄

不過用AbortController也不是一帆風順的。記得剛開始用的時候,我直接這樣寫:

// 錯誤示范,別學(xué)我
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();
    // 沒有重新創(chuàng)建controller!
  }
}

結(jié)果modal第二次打開的時候,ESC鍵失效了。原因很簡單:controller.abort()之后,這個controller就廢了,不能重復(fù)使用。

正確的寫法應(yīng)該是:

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();
    // 重新創(chuàng)建一個新的controller
    this.controller = new AbortController();
  }
}

真實項目:拖拽排序的坑

前段時間做一個看板功能,需要實現(xiàn)卡片拖拽排序。用傳統(tǒng)方式寫的話,光是事件監(jiān)聽器的管理就能把人逼瘋:

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;
    
    // 只在容器上監(jiān)聽mousedown
    this.container.addEventListener('mousedown', (e) => {
      const card = e.target.closest('.card');
      if (!card) return;
      
      this.startDrag(card, e);
    }, { signal });
  }
  
  startDrag(card, startEvent) {
    // 為每次拖拽創(chuàng)建獨立的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();
    
    // 創(chuàng)建拖拽副本
    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 });
    
    // 拖拽結(jié)束
    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();
  }
}

這種寫法的好處是,每次拖拽開始時創(chuàng)建獨立的controller,拖拽結(jié)束時自動清理相關(guān)事件。不會出現(xiàn)事件監(jiān)聽器累積的問題。

以前用傳統(tǒng)方式,我得手動管理mousemove和mouseup的清理,經(jīng)常出現(xiàn)拖拽結(jié)束后事件還在監(jiān)聽的bug。

React項目中的應(yīng)用

在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+都能用。我們項目的用戶主要是企業(yè)客戶,瀏覽器版本都比較新,所以直接用了。

如果你需要兼容老瀏覽器,可以加個簡單的判斷:

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 {
      // 降級到傳統(tǒng)方式
      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請求。直到那次內(nèi)存泄漏的事故,我才真正開始研究它的其他用法。

現(xiàn)在回頭看,這個API真的改變了我寫事件處理代碼的方式。代碼變得更簡潔,bug更少,維護成本也大大降低。

當然,不是說傳統(tǒng)的addEventListener就一無是處。在某些需要精確控制單個事件監(jiān)聽器的場景下,傳統(tǒng)方式可能還是有必要的。但對于大部分日常開發(fā),AbortController絕對是更好的選擇。

如果你也經(jīng)常被事件監(jiān)聽器的管理搞得頭疼,試試這個方法吧。保證你用了就回不去了。

轉(zhuǎn)自https://juejin.cn/post/7533211262761009188


該文章在 2025/8/5 8:36:28 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved