You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中使用useEffect返回回调仍无法移除事件监听器的原因及正确实现方式

为什么React组件的键盘事件监听器没有被正确移除?

问题描述

我有一个React组件,在初始化阶段注册了一个键盘事件,按下Enter键时会调用API获取随机数据。代码如下:

useEffect(() => { 
  const keyUpEvent = (event) => { 
    if (event.code === "Enter") { 
      event.preventDefault(); 
      fetchRandomData(); 
    } 
  }; 
  document.addEventListener("keyup", keyUpEvent); 
  fetchRandomData(); 
  return () => { 
    setChord({}); 
    document.removeEventListener("Keyup", keyUpEvent); 
  }; 
}, []);

但页面导航后,按下Enter键仍然会触发事件,多次导航后按一次按键会触发多次API调用,明显存在内存泄漏。想问为什么React没有移除已注册的事件监听器?正确的移除方式是什么?


解答

1. 核心原因:事件名称大小写不匹配

你代码里的问题非常隐蔽——事件名称是区分大小写的!

注册监听器时你用的是全小写的 "keyup":

document.addEventListener("keyup", keyUpEvent);

但在 cleanup 函数里移除时,写成了首字母大写的 "Keyup":

document.removeEventListener("Keyup", keyUpEvent);

浏览器会把这两个当成完全不同的事件类型,所以根本找不到要移除的监听器。组件卸载后,之前注册的keyup监听器还绑定在document上,多次页面导航就会积累多个监听器,按下Enter自然会触发多次API调用。

2. 正确的移除方式

只需要把 cleanup 里的事件名改成和注册时完全一致的全小写 "keyup" 即可:

useEffect(() => { 
  const keyUpEvent = (event) => { 
    if (event.code === "Enter") { 
      event.preventDefault(); 
      fetchRandomData(); 
    } 
  }; 
  document.addEventListener("keyup", keyUpEvent); 
  fetchRandomData(); 
  return () => { 
    setChord({}); 
    // 这里改成全小写的keyup
    document.removeEventListener("keyup", keyUpEvent); 
  }; 
}, []);

3. 额外的内存泄漏预防建议

除了修复事件名的问题,还有两个点可以进一步优化,避免潜在的内存泄漏:

  • 取消未完成的API请求:如果fetchRandomData是异步请求,组件卸载后请求还在进行的话,后续的状态更新(比如setChord)会触发警告,也会占用资源。可以用AbortController来取消请求:
useEffect(() => { 
  const abortController = new AbortController();
  const signal = abortController.signal;

  // 把fetch函数移到useEffect内部,确保能访问到signal
  const fetchRandomData = async () => {
    try {
      const res = await fetch('/your-api-url', { signal });
      const data = await res.json();
      setChord(data);
    } catch (err) {
      // 忽略取消请求的错误
      if (err.name !== 'AbortError') {
        console.error('请求失败:', err);
      }
    }
  };

  const keyUpEvent = (event) => { 
    if (event.code === "Enter") { 
      event.preventDefault(); 
      fetchRandomData(); 
    } 
  }; 

  document.addEventListener("keyup", keyUpEvent); 
  fetchRandomData(); 

  return () => { 
    abortController.abort(); // 取消未完成的请求
    setChord({}); 
    document.removeEventListener("keyup", keyUpEvent); 
  }; 
}, []);
  • 确保依赖项正确:如果fetchRandomData是在组件外部定义的函数,记得用useCallback包裹它,并添加到useEffect的依赖数组中,避免每次渲染创建新的函数实例,导致监听器无法正确移除。

内容的提问来源于stack exchange,提问作者Neyo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 18:49:05