防抖与节流:从原理到 React Hook 封装

前端高频事件如 scrollresizeinputclick 很容易在短时间内被大量触发,如果不加以控制,可能导致服务端压力激增、页面卡顿甚至逻辑错误。防抖和节流是两种经典的时间控制方案,但很多开发者对它们的区别和使用边界仍然模糊。本文从原理出发,手写实现,并封装适用于 React 的自定义 Hook,帮你彻底搞懂并在项目中正确落地。

一、防抖(Debounce)与节流(Throttle)的本质区别

防抖 :事件被触发后,等待一段时间,如果在这段时间内事件又被触发,则重新计时。只有在最后一次触发后的一段时间内没有再次触发 ,才执行目标函数。

典型场景 :搜索框输入联想、窗口 resize 结束后重新计算布局。

节流 :无论事件触发有多频繁,目标函数在固定时间间隔内最多只执行一次

典型场景:滚动加载更多、按钮防重复点击(但要配合状态防重)。

  • 防抖是"你一直点,我就不理你,等你停下来我才干活"。
  • 节流是"你尽管点,我每秒钟最多干一次"。

二、手写防抖和节流

1. 防抖(Debounce)

javascript 复制代码
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

使用示例:

javascript 复制代码
const handleInput = debounce((e) => {
  fetch(`/api/suggest?q=${e.target.value}`);
}, 300);

input.addEventListener('input', handleInput);

如果需要立即执行一次 (比如按钮点击第一次立即响应),可以增加一个 immediate 参数:

javascript 复制代码
function debounce(fn, delay = 300, immediate = false) {
  let timer = null;
  return function (...args) {
    const callNow = immediate && !timer;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!immediate) fn.apply(this, args);
    }, delay);
    if (callNow) fn.apply(this, args);
  };
}

2. 节流(Throttle)

时间戳版本(第一次立即执行):

javascript 复制代码
function throttle(fn, interval = 200) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

定时器版本(最后一次也会执行):

javascript 复制代码
function throttle(fn, interval = 200) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        fn.apply(this, args);
      }, interval);
    }
  };
}

实际中常用时间戳版本,保证第一次立即执行,适合滚动加载等需要即时响应的场景。

三、在 React 中封装自定义 Hook

在 React 函数组件中,普通的防抖/节流函数会遇到闭包问题------防抖函数内部捕获的 state 是旧值。需要用 useRef 保持最新的回调引用,或者配合 useCallback 进行封装。

1. useDebounce(对值进行防抖)

将频繁变化的值进行防抖,常用于搜索关键词:

tsx 复制代码
import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用
function SearchBox() {
  const [keyword, setKeyword] = useState('');
  const debouncedKeyword = useDebounce(keyword, 300);

  useEffect(() => {
    if (debouncedKeyword) {
      fetch(`/api/search?q=${debouncedKeyword}`);
    }
  }, [debouncedKeyword]);
}

这种方式将防抖逻辑和副作用分离,非常清晰。

2. useDebouncedCallback(防抖回调)

如果需要直接得到一个防抖后的函数,并且函数内部能访问到最新的 state,可以用 useRef 保存回调,避免过期闭包:

tsx 复制代码
import { useRef, useCallback } from 'react';

function useDebouncedCallback(fn: (...args: any[]) => void, delay: number) {
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const fnRef = useRef(fn);
  fnRef.current = fn; // 每次渲染保持最新

  return useCallback(
    (...args: any[]) => {
      if (timerRef.current) clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => {
        fnRef.current(...args);
      }, delay);
    },
    [delay]
  );
}

3. useThrottle(对值进行节流)

useDebounce 类似,保证在一定间隔内值只变化一次:

tsx 复制代码
function useThrottle<T>(value: T, interval: number): T {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastUpdated = useRef(Date.now());

  useEffect(() => {
    const now = Date.now();
    if (now - lastUpdated.current >= interval) {
      lastUpdated.current = now;
      setThrottledValue(value);
    } else {
      const timer = setTimeout(() => {
        lastUpdated.current = Date.now();
        setThrottledValue(value);
      }, interval - (now - lastUpdated.current));
      return () => clearTimeout(timer);
    }
  }, [value, interval]);

  return throttledValue;
}

4. useThrottledCallback(节流回调)

用于函数调用频率限制,例如滚动加载,内部同样使用 ref 保持最新回调:

tsx 复制代码
function useThrottledCallback(fn: (...args: any[]) => void, interval: number) {
  const lastTimeRef = useRef(0);
  const fnRef = useRef(fn);
  fnRef.current = fn;

  return useCallback(
    (...args: any[]) => {
      const now = Date.now();
      if (now - lastTimeRef.current >= interval) {
        lastTimeRef.current = now;
        fnRef.current(...args);
      }
    },
    [interval]
  );
}

四、真实场景落地

1. 搜索框输入联想(防抖值)

tsx 复制代码
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);

useEffect(() => {
  if (!debouncedQuery) return;
  fetch(`/api/suggest?q=${debouncedQuery}`)
    .then(res => res.json())
    .then(setSuggestions);
}, [debouncedQuery]);

2. 按钮防重复点击(防抖回调)

tsx 复制代码
const handleSubmit = useDebouncedCallback(async () => {
  await api.submit(formData);
  message.success('提交成功');
}, 1000);

return <Button onClick={handleSubmit}>提交</Button>;

注意:如果提交是异步且有 loading 状态,更推荐使用状态控制(disabled={loading})来从 UI 层面阻止重复点击,防抖只能作为辅助。

3. 滚动加载更多(节流回调)

tsx 复制代码
const handleScroll = useThrottledCallback(() => {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
    loadMore();
  }
}, 200);

useEffect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);

五、注意事项与常见坑

  1. 过期闭包 :在直接使用普通防抖函数时,如果内部读取了 React state,需要使用 ref 保持最新值,否则会读到旧的 state。
  2. 防抖时间设置:搜索输入通常 200-500ms;按钮防重复 500-1000ms;窗口 resize 300-500ms。
  3. 节流间隔设置:滚动 100-200ms;鼠标移动 50-100ms。
  4. 清理定时器:在组件卸载时务必清除定时器,否则会导致内存泄漏或尝试更新已卸载组件的状态。
  5. 防抖与节流的选择:如果关注的是"连续操作停止后的最终结果",用防抖;如果关注的是"操作过程中的平滑控制",用节流。

六、总结

防抖和节流是前端性能优化的基石,它们从时间维度上控制了昂贵操作的执行频率。在 React 中,将它们封装为 Hook 可以使代码更简洁、更符合声明式的编程习惯。

相关推荐
低代码布道师几秒前
外包项目管理平台全栈实战课02搭建数据底座:Next.js + PostgreSQL + Prisma 7
开发语言·javascript·postgresql·全栈开发
志尊宝14 分钟前
Vue3 零基础每日笔记(004):模板语法初识——插值表达式、v-bind、v-on 一次学会
javascript·vue.js·笔记
恋猫de小郭20 分钟前
Flutter 状态管理基准测评,一个很有趣的观点
android·前端·flutter
雪芽蓝域zzs26 分钟前
前端编辑组件wangEditor
前端
风骏时光牛马1 小时前
稳定性治理及疑难问题深度剖析与实战复盘
前端
JeffongTan9 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
不cong明的亚子11 小时前
web live多屏互动平台
javascript·经验分享·音视频
陆枫Larry11 小时前
Astro 是什么
前端
ocean210312 小时前
2025-2026年AI部署与MLOps大厂面试高频问题
人工智能·面试·大模型推理·ai部署