• 如何实现一个让面试官拍大腿的防抖节流函数


    “ 学防抖新文提旧话,习节流旧事又重提 ”

     

     程序员面试题库分享

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

    2、前端技术导航大全      推荐:★★★★★

    地址:前端技术导航大全

    3、开发者颜色值转换工具   推荐:★★★★★

    地址 :开发者颜色值转换工具

    这几天刚看看到了underscore.js的防抖和节流的部分,正好又去复习了这部分内容,于是又重新整理一下相关的知识点。

    在开发中我们经常会遇到一些高频操作,比如:鼠标移动,滑动窗口,键盘输入等等,节流和防抖就是对此类事件进行优化,降低触发的频率,以达到提高性能的目的。

    可以看到短短的几秒钟,触发的事件的次数是非常惊人的。

    防抖

    简单来说防抖就是无论触发多少次事件,但是我一定在事件触发后 n 秒后才执行,也就是最后一次触发完毕 n 秒后才执行,如果在 n 秒前又触发了,那么以新的事件的时间为准,重新开始计算时间。

    那么如何实现一个基本的防抖函数呢?

    基本实现

    根据防抖的原理可知,我们可以设置一个定时器,当每次触发事件但是没有到达设置的时间时,都会重新设置定时器。

    1.  const debounce = function(func, wait) {
    2.    let timeout
    3.    return function() {
    4.      // 再次触发事件则删除上一个定时器,重新设置
    5.      clearTimeout(timeout)
    6.      timeout = setTimeout(func, wait);
    7.   }
    8.  }

    这样我们就写出了一个最基本版的防抖函数。可以看到触发次数已经大大降低。

    this & arguments

    尽管上面已经实现了一个基本的防抖函数,但是依然是不完善的,比如在setTimeout中的this指向是无法正确的获取的,setTimeout中的this指向 Window 对象!

    我们可以在执行定时器之前进行重置this

    1.  const debounce = function(func, wait) {
    2.    let timeout
    3.    return function() {
    4.      // 保存this
    5.      let context = this // 新增
    6.  
    7.      clearTimeout(timeout)
    8.      timeout = setTimeout(function() {
    9.        func.apply(context) // 新增
    10.     }, wait);
    11.   }
    12.  }

    再比如我们如何在自定义的函数进行传参呢,如果我们想在func函数中传递event对象,目前的实现显然是无法正确进行获取参数的,再来修改一下:

    1.  const debounce = function(func, wait) {
    2.    let timeout
    3.    return function() {
    4.      let context = this // 新增
    5.      // 保存参数
    6.      let args = arguments // 新增
    7.  
    8.      clearTimeout(timeout)
    9.      timeout = setTimeout(function() {
    10.        func.apply(context, args) // 修改
    11.     }, wait);
    12.   }
    13.  }

    至此一个基本的防抖函数就已经实现了,这个函数已经很是非常完善了。

    立即执行

    接下来再增加一个功能,如果我们不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才重新触发执行。

    那么这个功能怎么做呢,其实可以这样想,我们可以传入一个参数immediate,代表是否想要立即执行,如果传递了immediate,则立即执行一次函数,然后设置一个定时器,时间截止后将定时器设置为null,下次进入函数时先判断定时器是否为null,然后决定是否再次执行。

    1.  const debounce = function(func, wait, immediate) {
    2.    let res, timeout, context, args;
    3.  
    4.    const debounced = function() {
    5.      context = this
    6.      args = arguments
    7.      // 如果已经设置了setTimeout,则重新进行设置
    8.      if(timeout) clearTimeout(timeout)
    9.      // 判断是否为立即执行
    10.      if(immediate) {
    11.        let runNow = !timeout
    12.        // 设置定时器,指定时间后设置为null
    13.        timeout = setTimeout(function() {
    14.          timeout = null
    15.       }, wait)
    16.        // 如果timeout已经为null(已到期),则执行函数
    17.        // 保存执行结果,用于函数返回
    18.        if(runNow) res = func.apply(context, args)
    19.     } else {
    20.        // 如果没有设置立即执行,则设置定时器
    21.        timeout = setTimeout(function() {
    22.          func.apply(context, args)
    23.       }, wait)
    24.     }
    25.      return res
    26.   }
    27.  
    28.    return debounced
    29.  }

    其实上面的实现是两种完全不同的触发方式,先来看一下流程图:

    黑色箭头为触发动作,红色箭头为执行动作。

    非立即执行

    立即执行

    来看一下执行流程: 首先如果immediate为true的情况:

    第一次执行:timeoutnull,则runNowtrue,然后设置一个定时器,在指定的时间后设置timeoutnull,这也就代表设置执行的间隔时间,最后判断runNow是否执行函数。

    第二次执行:

    • 情况一:已超过设置时间:如果第二次触发执行已经超过设置的时间,此时timeout已经被定时器设置为null,那么进入debounced函数后,runNowtrue,重新设置定时器,然后执行函数。
    • 情况二:未超过设置时间:因为没有超过设置时间,所以timeout并未被定时器设置为null,那么runNowfalse,由于timeout的定时器已经被清除,所以重置定时器,不会执行函数。

    再来看一下immediatefalse的情况:

    其实这种情况和我们之前设置的是一样的,没有超过设置时间,则重置定时器,定时器在到达指定时间后自动执行一次函数。

    两者之间最大的区别是:立即执行的功能会在第一次触发函数的时候执行一次,下次触发如果已到达设置时间,则直接执行一次。而非立即执行的功能第一次触发函数时只会设置一个定时器,时间到达后自动执行,如果在设置时间内触发只会重置定时器,永远不会立即执行函数。

    取消

    再增加一个需求:如果想要取消debounce函数怎么办,比如 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样只有等 10 秒后才能重新触发事件,如果有一个取消功能,点击后取消防抖,再去触发,就可以立刻执行了。

    1.  debounced.cancel = function() {
    2.      // 删除定时器
    3.      clearTimeout(timeout);
    4.      // 设置timeout为null
    5.      timeout = null;
    6.  };

    只需要将定时器清除,设置timeoutnull即可,因为如果immediate 为 true会直接执行一次函数,然后重新设置定时器 😂

    完整实现:

    最后完整的防抖函数如下:

    1.  function debounce(func, wait, immediate) {
    2.    let res, timeout, context, args;
    3.  
    4.    const debounced = function () {
    5.        context = this;
    6.        args = arguments;
    7.  
    8.        if (timeout) clearTimeout(timeout);
    9.        if (immediate) {
    10.            var runNow = !timeout;
    11.            timeout = setTimeout(function(){
    12.                timeout = null;
    13.           }, wait)
    14.            if (runNow) res = func.apply(context, args)
    15.       }
    16.        else {
    17.            timeout = setTimeout(function(){
    18.                func.apply(context, args)
    19.           }, wait);
    20.       }
    21.        return res;
    22.   };
    23.  
    24.    debounced.cancel = function() {
    25.        clearTimeout(timeout);
    26.        timeout = null;
    27.   };
    28.  
    29.    return debounced;
    30.  }

    节流

    节流也是用于减少触发执行的手段之一,但是思路和防抖是完全不一样的,

    如果持续触发事件,每隔一段时间,只执行一次事件。也就是只按照设置的时间作为时间段,到达指定的时间后触发函数就会执行。没有到达指定的时间,无论如何触发函数都不会执行。

    也就是没到点,无论你怎么撩,我都岿然不动 🤩

    目前有两种实现方式:使用时间戳和设置定时器。

    时间戳

    当触发函数的时候,使用当前的时间戳与上一次触发函数所保存的时间戳相减,然后对比设置定时器的时间,决定是否执行函数。

    1.  const throttle = function(func, wait) {
    2.    let previous = 0, context, args;
    3.  
    4.    return function() {
    5.      context = this
    6.      args = arguments
    7.  
    8.      // 获取当前时间戳
    9.      let now = +new Date()
    10.      // 判断当前时间戳与上一次触发的时间差值是否大于等于指定时间
    11.      if((now - previous) >= wait) {
    12.        func.apply(context, args)
    13.        // 更新时间戳
    14.        previous = now
    15.     }
    16.   }
    17.  }

    值得注意的是:js中可以在某个元素前使用 '+' 号,这个操作是将该元素转换成Number类型,如果转换失败,那么将得到 NaN

    +new Date() 将会调用 Date.prototype 上的 valueOf() 方法,根据MDN,Date.prototype.value方法等同于Date.prototype.getTime()

    1.  console.log(+new Date('2022-08-17'));
    2.  console.log(new Date('2022-08-17').getTime());
    3.  console.log(new Date('2022-08-17').valueOf());
    4.  console.log(new Date('2022-08-17') * 1);
    5.  // 结果都是相同的

    设置定时器

    设置定时器的实现思路是:在第一次触发时设置一个定时器,在指定时间之后设置变量为null,下次触发函数判断变量是否为null,来决定是否执行函数。

    1. const throttle = function(func, wait) {
    2. let timeout, context, args;
    3. return function() {
    4. context = this
    5. args = arguments
    6. // 允许执行
    7. if(!timeout) {
    8. // 设置定时器,到达时间后设置timeout为null
    9. timeout = setTimeout(function() {
    10. timeout = null
    11. func.apply(context, args)
    12. }, wait)
    13. }
    14. }
    15. }

    以上两种方式均可以满足一个基本的节流函数的写法,但是两种写法还是有一定的区别的:

    1. 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
    2. 第一种事件停止触发后不会再执行事件,第二种事件停止触发后依然会再执行一次事件

    既然执行时的行为不同,那么有没有办法将两者结合呢?

    两者结合

    将两者结合起来是要实现一个既能开始时执行一次函数,又能结束时再执行一次函数!

    思路是这样的:如果触发函数时没有到达指定时间,则设置定时器,如果已经到达设置的时间,则直接进行执行。

    1. function throttle(func, wait) {
    2. let timeout, context, args, previous = 0;
    3. const later = function() {
    4. // 定时器执行时更新时间戳
    5. previous = +new Date();
    6. timeout = null;
    7. // 执行函数
    8. func.apply(context, args)
    9. };
    10. const throttled = function() {
    11. let now = +new Date();
    12. //下次触发 func 剩余的时间
    13. let remaining = wait - (now - previous);
    14. context = this;
    15. args = arguments;
    16. // 如果没有剩余的时间了或者更改了系统时间
    17. if (remaining <= 0 || remaining > wait) {
    18. // 清空定时器及timeout
    19. if (timeout) {
    20. clearTimeout(timeout);
    21. timeout = null;
    22. }
    23. // 更新时间戳变量
    24. previous = now;
    25. func.apply(context, args);
    26. } else if (!timeout) {
    27. // 处理还没有到达指定时间的触发行为
    28. // 此处设置定时器时间要设置剩余的时间,与上文中防抖函数中有区别
    29. timeout = setTimeout(later, remaining);
    30. }
    31. };
    32. return throttled;
    33. }

    还是依旧缕一下思路:

    第一次触发 throttled 时,因为 previous 为 0 ,所以remaining <= 0这个条件成立,执行func函数,并且重置定时器及变量,最后将previous跟更新为当前时间。

    第二次触发:

    1. 未到达指定时间:如果没有到达指定时间,那么remaining为正数,所以不会进入remaining <= 0这个执行语句,而是会设置定时器。不会执行函数。
    2. 到达指定时间:remaining为负数,执行函数,同第一次触发。

    同样在定时器执行时,也会更新previoustimeout的值。

    其实核心在于remaining这个变量的运算。

    控制执行时机

    又又又来了一个需求,如果希望能够控制首次和末次要不要执行怎么办?

    可以传递第三个参数:

    • leading:false 表示禁用第一次执行
    • trailing: false 表示禁用停止触发的回调
    1. function throttle(func, wait, options = {}) { //修改
    2. let timeout, context, args, previous = 0;
    3. const later = function() {
    4. previous = options.leading === false ? 0 : +new Date(); //修改
    5. timeout = null;
    6. func.apply(context, args);
    7. // 清空作用域及参数变量
    8. if (!timeout) context = args = null; //修改
    9. };
    10. const throttled = function() {
    11. let now = +new Date();
    12. // 如果是首次触发,并且设置首次不执行函数。那么将previous与now进行同步
    13. // now 与 previous 相减不小于0,则不会执行函数
    14. if (!previous && options.leading === false) previous = now; // 新增
    15. let remaining = wait - (now - previous);
    16. context = this;
    17. args = arguments;
    18. if (remaining <= 0 || remaining > wait) {
    19. if (timeout) {
    20. clearTimeout(timeout);
    21. timeout = null;
    22. }
    23. previous = now;
    24. func.apply(context, args);
    25. // 清空作用域及参数变量
    26. if (!timeout) context = args = null; //修改
    27. } else if (!timeout && options.trailing !== false) { // 修改
    28. timeout = setTimeout(later, remaining);
    29. }
    30. };
    31. return throttled;
    32. }

    我们要注意的是实现中有这样一个问题:

    那就是 leading:false 和 trailing: false 不能同时设置。因为如果同时设置,那么就是既不开始触发也不结束时触发,那么函数将不会正常执行。

    其实核心还是关于时间戳的加减法,无非就是根据功能来设置时间戳而已。

    取消

    与防抖函数的取消功能基本相同,重置各个作用变量:

    1. throttled.cancel = function() {
    2. clearTimeout(timeout);
    3. previous = 0;
    4. timeout = null;
    5. }

    完整实现

    1. function throttle(func, wait, options = {}) {
    2. let timeout, context, args, previous = 0;
    3. const later = function() {
    4. previous = options.leading === false ? 0 : +new Date();
    5. timeout = null;
    6. func.apply(context, args);
    7. if (!timeout) context = args = null;
    8. };
    9. const throttled = function() {
    10. let now = +new Date();
    11. if (!previous && options.leading === false) previous = now;
    12. let remaining = wait - (now - previous);
    13. context = this;
    14. args = arguments;
    15. if (remaining <= 0 || remaining > wait) {
    16. if (timeout) {
    17. clearTimeout(timeout);
    18. timeout = null;
    19. }
    20. previous = now;
    21. func.apply(context, args);
    22. if (!timeout) context = args = null;
    23. } else if (!timeout && options.trailing !== false) {
    24. timeout = setTimeout(later, remaining);
    25. }
    26. throttled.cancel = function() {
    27. clearTimeout(timeout);
    28. previous = 0;
    29. timeout = null;
    30. }
    31. };
    32. return throttled;
    33. }

    这也是underscore.js中节流的实现方式。

     最后分享程序员面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

    2、前端技术导航大全      推荐:★★★★★

    地址:前端技术导航大全

    3、开发者颜色值转换工具   推荐:★★★★★

    地址 :开发者颜色值转换工具

  • 相关阅读:
    8 财政收入预测分析
    如何快速通过pmp考试求攻略
    16.2 ARP 主机探测技术
    vite动态配置svg图标及其他方式集合
    美团外卖搜索基于Elasticsearch的优化实践
    常见树种(贵州省):006栎类
    制作网页HTML,CSS
    el-form动态检验rules
    4、android中级控件(2)(选择按钮)
    【电力系统】含电热联合系统的微电网运行优化附matlab代码和复现论文
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/126430827