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

自定义React Hook中setTimeout未正确清除多次触发问题排查

问题原因

你代码里有两个核心错误导致定时器多次触发:

  1. 错误引入了Node.js专属的定时器
    自定义Hook里的import { setTimeout } from 'timers';是Node.js环境的模块导入语句,浏览器环境不存在这个模块的实现,导入后会导致定时器的清理逻辑完全失效,你可以直接使用浏览器全局挂载的setTimeout、clearTimeout,不需要额外导入。

  2. useEffect依赖项配置错误
    你把screenWidth加到了useEffect的依赖数组中,导致每次screenWidth更新时,useEffect都会重新执行:首先执行上一轮effect的清理函数移除旧的resize监听,再注册新的resize监听。而上一轮effect闭包中创建的未执行定时器,在本轮effect中完全访问不到,你的clearTimeout只能清掉当前闭包内的定时器,历史遗留的定时器就会正常触发,最终表现为回调多次执行。
    你之前写在组件内的逻辑实际上也有同样的问题,只是因为你设置的延时只有100ms,触发频率低不容易感知到而已。


修复方案

修改后的自定义Hook代码如下:

import { useEffect, useState, useRef } from 'react';

export const useDebounceScreenWidth = (delay = 1000) => {
  const [screenWidth, setScreenWidth] = useState(window.innerWidth);
  // 用useRef存储定时器ID,保证所有闭包都能访问到同一个引用
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    const listener = () => {
      if (timerRef.current) clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => {
        setScreenWidth(window.innerWidth);
        console.log(screenWidth);
      }, delay);
    };

    window.addEventListener('resize', listener);
    // 初始调用一次获取正确的初始宽度
    listener();

    return () => {
      window.removeEventListener('resize', listener);
      // 组件卸载时清理剩余定时器
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  // 依赖项设为空数组,只在Hook初始化时执行一次
  }, []);

  return [screenWidth] as const;
};

修改点说明:

  • 移除了错误的Node.js定时器导入
  • useEffect依赖改为空数组,仅注册一次监听事件
  • 用useRef存储定时器ID,保证所有闭包都能访问到同一个定时器引用,不会出现清不掉历史定时器的问题
  • 增加了初始调用和卸载时的定时器清理逻辑,避免内存泄漏
  • 给返回值加了as const类型声明,调用时可以正确推导screenWidth是number类型,不会被识别为(number | void)[]

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 08:54:04