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

处理事件监听器时如何在React useEffect内实现防抖

实现方案

首先修正你原有代码的一个显性bug:effect 清理函数中错误使用addEventListener绑定事件,会导致监听器无法被正确移除,引发内存泄漏,正确逻辑应该调用removeEventListener。

你的滚动触底场景更适合用节流而非防抖:防抖会在用户停止滚动1秒后才触发请求,存在明显的加载延迟;节流可以保证1秒内最多触发一次请求,触底时立刻响应,既避免重复调用,也符合无限滚动的交互预期。如果确实需要防抖效果,下文也会给出对应修改方式。

核心解决思路是用useRef存储需要跨渲染周期保留的变量(定时器标识、最新的请求方法),既解决闭包捕获陈旧状态的问题,也不需要每次状态变化都重新绑定滚动事件,性能更优。


无第三方依赖的原生实现

import { useEffect, useRef } from 'react';

// 组件内部代码
// 存储节流定时器标识
const scrollTimerRef = useRef(null);
// 存储最新的fetchBookList方法,避免闭包拿到旧值
const latestFetchRef = useRef(fetchBookList);
// 每次渲染同步最新的方法引用到ref
latestFetchRef.current = fetchBookList;

useEffect(() => {
  const handleScroll = () => {
    const isReachBottom = Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight;
    if (!isReachBottom) return;

    // 节流逻辑:1秒内已存在待执行任务则直接跳过
    if (scrollTimerRef.current) return;

    scrollTimerRef.current = setTimeout(() => {
      latestFetchRef.current();
      // 执行完成后清空标识,允许下一次触发
      scrollTimerRef.current = null;
    }, 1000);
  };

  window.addEventListener('scroll', handleScroll, { passive: true });

  return () => {
    window.removeEventListener('scroll', handleScroll);
    // 组件卸载时清理定时器,避免内存泄漏
    if (scrollTimerRef.current) {
      clearTimeout(scrollTimerRef.current);
      scrollTimerRef.current = null;
    }
  };
// 依赖数组留空即可,仅在组件挂载时绑定一次事件
}, []);

如果需要替换为防抖效果(滚动停止1秒后才发请求),只需要把节流判断的逻辑替换为如下代码即可:

// 防抖逻辑:每次触发都重置计时
if (scrollTimerRef.current) {
  clearTimeout(scrollTimerRef.current);
}
scrollTimerRef.current = setTimeout(() => {
  latestFetchRef.current();
  scrollTimerRef.current = null;
}, 1000);

引入工具库的实现(以lodash为例)

如果项目中已经引入了lodash等通用工具库,可以直接使用其封装的throttle/debounce方法,注意要用useRef存储包装后的函数,避免每次渲染重新生成实例导致节流/防抖失效:

import { useEffect, useRef } from 'react';
import { throttle } from 'lodash-es';

// 组件内部代码
const latestFetchRef = useRef(fetchBookList);
latestFetchRef.current = fetchBookList;
// 初始化节流函数,仅创建一次
const throttledFetchRef = useRef(
  throttle(() => {
    latestFetchRef.current();
  }, 1000, { leading: true, trailing: false })
);

useEffect(() => {
  const handleScroll = () => {
    const isReachBottom = Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight;
    if (isReachBottom) {
      throttledFetchRef.current();
    }
  };

  window.addEventListener('scroll', handleScroll, { passive: true });

  return () => {
    window.removeEventListener('scroll', handleScroll);
    // 组件卸载时取消待执行的任务
    throttledFetchRef.current.cancel();
  };
}, []);

注意事项

  • 不要在useEffect回调内直接创建throttle/debounce实例,同时把bookList/bookType等状态放进依赖数组:这种写法每次依赖变化都会生成新的节流/防抖实例,之前的计时状态会丢失,依然会出现短时间重复触发请求的问题。
  • 所有需要在滚动回调中访问的组件状态、方法,都可以参考latestFetchRef的写法,通过ref同步最新引用,完全规避闭包陈旧问题,不需要为了拿到最新值反复绑定解绑事件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 07:39:29