处理事件监听器时如何在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
相关产品推荐
相关产品推荐

