防抖节流 + 在React组件中的使用踩坑

Posted by CodingWithAlice on July 26, 2025

防抖节流 + 在React组件中的使用踩坑

总结:

1、平时键盘抬起就会搜索,防抖后,间隔一段时间不输入才会搜索节流后,在频繁触发的事件流中,函数以固定的频率执行

2、在 组件中实现 防抖时,需要注意配合 useCallback 避免 组件重新渲染导致 定时器失效

3、使用防抖后,需要清理

一、实际应用 - 踩坑记录

1、使用场景

  • 防抖 debounce
    • search 搜索联想,用户在不断输入值时,用防抖来节约请求资源

    • 浏览器 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

  • 节流 throttle
    • 鼠标不断点击触发,mousedown (单位时间内只触发一次)
    • 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断

2、防抖在 React 组件中使用时,需要配合 useCallback

const handleFunc = useCallback( debounce(e => {}, 50), [])

问题:防抖失效

原因:在 React 组件中,如果 直接写 const fn = debounce(…),每次 组件渲染 都会 生成新的 debounce 实例,导致防抖失效 -> 每次都是新的定时器

解决方案:useCallback 返回一个记忆化的回调函数,只有依赖项变化时才会重新生成 -> 保证 debounce 的定时器和状态能持续生效

3、防抖需要清除!!

问题:onMouseMove 函数防抖后,鼠标离开表格,但防抖的延迟执行是异步的:即使事件停止触发,最后一次调用仍可在未来执行

原因:鼠标离开表格,若防抖延迟(50ms)未结束,最后一次 onMouseMove 仍会执行

解决方案:防抖是需要清除的

清理时机:

  • 组件卸载,useEffect的清理函数 或 componentWillUnMount
  • 事件监听移除前
  • 依赖变化时
useEffect(() => {
    return () => onMouseMove.cancel() // 1、lodash 实现的防抖函数使用 cancel 来清理 timeId
}, [])
const onMouseLeave = useCallback(() => {
    onMouseMove.cancel() // 2、函数变更,取消未执行的防抖函数
}, [onMouseMove])

####

4、lodash 的节流 可以配置属性

如果频繁调用,可以配置 throttle 的 leading: true trailing:true 决定是否在头部/尾部执行

二、实现

1、 函数防抖

类似法师技能读条,还没读完条再按技能,会重新读条 -> 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时

  • 防抖原理:触发事件,但是 在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,那就 以新的事件触发的时间为准,n 秒后才执行,–> 即要等 触发完事件 n 秒内不再触发事件,才会执行事件

  • 实现防抖的 典型逻辑 是通过设置一个定时器,每次事件触发时清除之前的定时器,然后重新设置一个新的定时器。只有当最后一次触发后经过了设定的延迟时间,定时器中的函数才会执行。

      function debounce(fn, delay) {
          let timeId = null;
          return function (...args) {
              // 清理定时器,才能保证不停读条-覆盖上一次
              timeId && clearTimeout(timeId);
              timeId = setTimeout(() => {
                  fn.apply(this, ...args);
              }, delay)
          }
      }
    
  • 常见错误:

    1、记得清理定时器,才能够做到【间隔一段时间不输入才会执行回调

    image-20241213222902041

2、函数节流

就是 一直按着技能键,也能在规定时间内发技能 - 解决被触发频率太高的问题

  • 节流通常是通过一个 标记变量和定时器 来实现,当函数执行后,设置一个定时器,在定时器未结束期间(即未达到规定的时间间隔),再次触发事件时函数不会执行,直到定时器结束,标记变量重置,函数才可以再次执行。

  • 常见错误:

    1、不用再清理定时器来处理了,也就不用记录

    2、setTimeout用来管理 flag的值,即可控制

    3、第一次是会触发的

    image-20241221145051497

      // 不是使用计算时间的方式来判断,而是直接使用 flag标记变量
      function throttle(fn, delay) {
          let flag = true;
          return function (...args) {
              if (!flag) {
                  return;
              }
              fn.apply(this, args);
              flag = false;
              setTimeout(() => {
                  flag = true;
              }, delay);
          };
      }
    

三、细节

1、函数防抖 - 节约开销

案例:一段键盘输入就执行函数的代码 - 如图,存在抖动问题

image-20210710163059697

将函数进行防抖处理 - 优化后:

function debounce(fn, delay) {
    let timeId = null;
    return function (...args) {
        if(timeId) {
            clearTimeout(timeId);
        }
        timeId = setTimeout(() => {
            fn(...args);
        }, delay)
    }
}

// 添加防抖处理
let keyUp = (value) => console.log(value);
let debounceKeyUp = debounce(keyUp, 1000);

// 给指定 input 添加监听函数
let input = document.getElementById('input');
input.addEventListener('keyup', (e) => {
    debounceKeyUp(e.target.value);
})

image-20210710163129879

补充:这里有一个很有趣的例子 - setInterval 里面包裹 setTimeout,讨论下时间

  • 如果外部的 delay 时间大于 防抖的 delay 时间 - 函数最终以外部时间周期执行

      let show = function () {console.log(1);}
      // 第一次 3秒,之后每次都是 2秒 执行一次
      setInterval(debounce(show, 1000), 2000);
      // 第一次 4秒,之后每次都是 3秒 执行一次
      setInterval(debounce(show, 1000), 3000);
    

    原因分析如图:

    image-20210710195651390

    image-20210710195948437

  • 如果外部的 delay 时间小于 防抖的 delay 时间 - show 函数不会执行

      setInterval(debounce(show, 4000), 3000);
    

    原因分析如图:

    image-20210710200524848

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

2、节流

// 在不断输入时,ajax会按照我们设定的时间,每1s执行一次
let throttle = (callback, delay) => {
    let last;
    return (arguments) => {
        let now = +new Date();
        if(last && now < last + delay) {
            clearTimeout(callback.timeId);
            callback.timeId = setTimeout(() => {
                last = now;
                callback.call(this, arguments)
            }, delay)
        } else {
            // 如果现在 = 上一次的时间 + 延迟时间,那么就马上执行函数
            last = now;
            callback.call(this, arguments)
        }
    }
}
// 可以简化成 flag 来实现 - 无法处理异步场景
function throttle(fn, delay) {
    let timer = null;
    let flag = true;
    return function (...args) {
        if (!flag) {
            return;
        }
        fn.apply(this, args);
        flag = false;
        timer = setTimeout(() => {
            flag = true;
        }, delay);
    };
}

let throttleAjax = throttle(ajax, 1000);
let input3 = document.getElementById('input3')
input3.addEventListener('keyup', (e) => {
    throttleAjax(e.target.value);
});

image-20210710203800964