防抖debounce理解

1,275 阅读5分钟

「我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

防抖相信大家都不陌生,面试中会经常会被问题或提起。比如会问一些前端优化、手写防抖节流函数等等,这里就跟着underscore 源码来学习一下。

定义

在规定时间后才执行,如果触发则重新计时 也就是说,防抖函数在n秒内,无论触发了多少次函数回调,我都只只在n秒后执行一次。比如我们设置一个等待时间为5秒的防抖函数,如果5秒内有触发,就需要重新计时,直到5秒内没有触发就调用执行。

使用场景

最近项目中有一个表单搜索场景,在输入文字的过程中会持续触发oninput事件,而搜索接口只是在用户输入搜索文字后进行调用。如果是用户输入一个文字就搜索一次,不仅会频繁调用后台接口,前端显示效果也不好。

使用防抖的话,可以将接口调用设定在500ms内没有触发oninput事件后再调用接口,这样就可以解决问题。

还会在其他场景使用

  • 一些频繁点击操作的按钮,比如登录、短信验证,避免用户短时间多次发送
  • 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  • 鼠标移动mousedown计算等场景

实现原理

实现原理其实很简单,就是利用定时器,函数在最开始执行的时候就设定一个定时器,如果在n秒内有执行就吧定时器清空,重新设定一个新的定时器,当n秒内没有再调用后,定时器计时结束后就会触发回调。

第一版

/**
* debounce防抖
* @param { function } fn 回调
* @param { number } wait 等待时间
*/
function debounce(fn, wait = 300) {
    // 利用闭包生成唯一的一个定时器
    let timer = null;

    // 返回一个函数,当作触发事件执行
    return function (...args) {
      if (timer) {
        // 上一次存在定时器,需要清空
        clearTimeout(timer);
      }
      // 设定定时器,定时器结束后执行回调函数 fn  如果多次触发就重新设定
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, wait);
    };
}

我们再写一个输入框事件来测试一下


<input type="text" oninput="oninputHandler(event)" />

<script>
    const testFn = debounce((event) => {
        console.log('执行防抖', event.target.value);
    }, 1000);

    // 执行防抖 停止 scroll 事件后 1 秒执行回调
    function oninputHandler(event) {
        testFn(event);
    }

    // 不执行防抖
    function oninputHandler(event) {
        console.log('input change value: ' + event.target.value);
    }
</script>

这是没有执行防抖 Kapture 2022-12-04 at 21.56.21.gif

开启防抖后 Kapture 2022-12-04 at 21.57.44.gif

效果还是很明显的,从原来的输入一个值就触发,到现在1秒内没有输入才触发,至此,简单版防抖就已经实现了。

第二版

接下来再来对防抖做一下改造,在首次调用的时候立即执行函数,等到n秒内没有触发,才可以重新触发执行。

听起来有点绕,也就是说在oninput事件第一次触发的时候就执行,后续的触发都不执行。等到1秒内没有执行后,再触发oninput时又会执行第一次。

/**
* debounce防抖
* @param { function } fn 回调
* @param { number } wait 等待时间
* @param { boolean } immediate 是否立即执行
*/
function debounce(fn, wait = 300, immediate = false) {
    // 利用闭包生成唯一的一个定时器
    let timer = null;

    // 返回一个函数,当作触发事件执行
    return function (...args) {
      if (timer) {
        // 上一次存在定时器,需要清空
        clearTimeout(timer);
      }

      // immediate: true 时,首次触发后立即执行
      if (immediate) {
        // 是否首次执行过
        const isExecute = !timer;

        // 赋值定时器 避免重复执行
        timer = setTimeout(() => {
          timer = null;
        }, wait);

        // 首次执行
        isExecute && fn.apply(this, args);
      } else {
        // 设定定时器,定时器结束后执行回调函数 fn  如果多次触发就重新设定
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, wait);
      }
    };
}

underscore 源码

来看一下underscore里是如何实现的,先将核心代码复制出来,用上面的oninput事件来调试,看一下它的一个具体步骤。

debounced方法内部打上一个断点,然后在输入框输入数据触发防抖

function debounce(func, wait, immediate) {
  var timeout, previous, args, result, context;

  var later = function () {
    // now获取的是当前时间 previous 会在第一次进入的时候记录  对比两个时间差是否小于 wait 等待时间
    var passed = now() - previous;

    if (wait > passed) {
      // 小于等待时间 说明在 wait时间内有触发 重新设定定时器
      timeout = setTimeout(later, wait - passed);
    } else {
      // 超过等待时间 执行回调
      // 清空 timeout  避免影响到下次使用
      timeout = null;

      // 判断是否立即执行
      if (!immediate) result = func.apply(context, args);

      // This check is needed because `func` can recursively invoke `debounced`.
      // 清空上下文、arguments 参数 在回调里面嵌套使用
      if (!timeout) args = context = null;
    }
  };
  // 先执行这里  通过 restArguments 将处理结果当作函数进行返回 回调时传递 arguments 参数
  var debounced = restArguments(function (_args) {
    context = this;
    args = _args;
    // 触发一次记录时间  用来和等待时间对比
    previous = now();
    if (!timeout) {
      // 第一次进入时执行
      // 执行 later 函数
      timeout = setTimeout(later, wait);

      // 立即执行
      if (immediate) result = func.apply(context, args);
    }
    return result;
  });

  // 取消执行 清空定时器等参数
  debounced.cancel = function () {
    clearTimeout(timeout);
    timeout = args = context = null;
  };

  return debounced;
}

源码还是有很多亮点的

  • 增加了cancel方法,可以随时取消。

  • 在执行回调的时候,吧函数结果当作返回值return出去,是为了避免回调中有返回数据。

  • 通过记录每次执行时间差,来判断是否需要执行回调。