利用JavaScript实现防抖节流函数的示例代码

 更新时间:2022年08月18日 09:46:39   作者:pino  
在开发中我们经常会遇到一些高频操作,比如:鼠标移动,滑动窗口,键盘输入等等,节流和防抖就是对此类事件进行优化,降低触发的频率,以达到提高性能的目的。本文就教大家如何实现一个让面试官拍大腿的防抖节流函数,需要的可以参考一下

最近在看红楼梦,看的诗词多了,时不时的也想来一句...

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

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

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

防抖

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

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

基本实现

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

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

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

this & arguments

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

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

 const debounce = function(func, wait) {
   let timeout
   return function() {
     // 保存this
     let context = this // 新增
 
     clearTimeout(timeout)
     timeout = setTimeout(function() {
       func.apply(context) // 新增
     }, wait);
   }
 }

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

 const debounce = function(func, wait) {
   let timeout
   return function() {
     let context = this // 新增
     // 保存参数
     let args = arguments // 新增
 
     clearTimeout(timeout)
     timeout = setTimeout(function() {
       func.apply(context, args) // 修改
     }, wait);
   }
 }

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

立即执行

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

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

 const debounce = function(func, wait, immediate) {
   let res, timeout, context, args;
 
   const debounced = function() {
     context = this
     args = arguments
     // 如果已经设置了setTimeout,则重新进行设置
     if(timeout) clearTimeout(timeout)
     // 判断是否为立即执行
     if(immediate) {
       let runNow = !timeout
       // 设置定时器,指定时间后设置为null
       timeout = setTimeout(function() {
         timeout = null
       }, wait)
       // 如果timeout已经为null(已到期),则执行函数
       // 保存执行结果,用于函数返回
       if(runNow) res = func.apply(context, args)
     } else {
       // 如果没有设置立即执行,则设置定时器
       timeout = setTimeout(function() {
         func.apply(context, args)
       }, wait)
     }
     return res
   }
 
   return debounced
 }

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

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

非立即执行

立即执行

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

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

第二次执行:

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

再来看一下immediatefalse的情况:

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

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

取消

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

 debounced.cancel = function() {
     // 删除定时器
     clearTimeout(timeout);
     // 设置timeout为null
     timeout = null;
 };

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

完整实现

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

 function debounce(func, wait, immediate) {
   let res, timeout, context, args;
 
   const debounced = function () {
       context = this;
       args = arguments;
 
       if (timeout) clearTimeout(timeout);
       if (immediate) {
           var runNow = !timeout;
           timeout = setTimeout(function(){
               timeout = null;
           }, wait)
           if (runNow) res = func.apply(context, args)
       }
       else {
           timeout = setTimeout(function(){
               func.apply(context, args)
           }, wait);
       }
       return res;
   };
 
   debounced.cancel = function() {
       clearTimeout(timeout);
       timeout = null;
   };
 
   return debounced;
 }

节流

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

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

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

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

时间戳

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

 const throttle = function(func, wait) {
   let previous = 0, context, args;
 
   return function() {
     context = this
     args = arguments
 
     // 获取当前时间戳
     let now = +new Date()
     // 判断当前时间戳与上一次触发的时间差值是否大于等于指定时间
     if((now - previous) >= wait) {
       func.apply(context, args)
       // 更新时间戳
       previous = now
     }
   }
 }

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

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

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

设置定时器

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

const throttle = function(func, wait) {
  let timeout, context, args;

  return function() {
    context = this
    args = arguments
    // 允许执行
    if(!timeout) {
      // 设置定时器,到达时间后设置timeout为null
      timeout = setTimeout(function() {
        timeout = null
        func.apply(context, args)
      }, wait)
    }
  }
}

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

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

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

两者结合

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

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

function throttle(func, wait) {
  let timeout, context, args, previous = 0;

  const later = function() {
      // 定时器执行时更新时间戳
      previous = +new Date();
      timeout = null;
      // 执行函数
      func.apply(context, args)
  };

  const throttled = function() {
      let now = +new Date();
      //下次触发 func 剩余的时间
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果没有剩余的时间了或者更改了系统时间
      if (remaining <= 0 || remaining > wait) {
          // 清空定时器及timeout
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          // 更新时间戳变量
          previous = now;
          func.apply(context, args);
      } else if (!timeout) {
          // 处理还没有到达指定时间的触发行为
          // 此处设置定时器时间要设置剩余的时间,与上文中防抖函数中有区别
          timeout = setTimeout(later, remaining);
      }
  };
  return throttled;
}

还是依旧缕一下思路:

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

第二次触发:

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

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

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

控制执行时机

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

可以传递第三个参数:

  • leading:false 表示禁用第一次执行
  • trailing: false 表示禁用停止触发的回调
function throttle(func, wait, options = {}) { //修改
  let timeout, context, args, previous = 0;

  const later = function() {
      previous = options.leading === false ? 0 : +new Date(); //修改
      timeout = null;
      func.apply(context, args);
      // 清空作用域及参数变量
      if (!timeout) context = args = null; //修改
  };

  const throttled = function() {
      let now = +new Date();
      // 如果是首次触发,并且设置首次不执行函数。那么将previous与now进行同步
      // now 与 previous 相减不小于0,则不会执行函数
      if (!previous && options.leading === false) previous = now; // 新增
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          previous = now;
          func.apply(context, args);
          // 清空作用域及参数变量
          if (!timeout) context = args = null; //修改
      } else if (!timeout && options.trailing !== false) { // 修改
          timeout = setTimeout(later, remaining);
      }
  };
  return throttled;
}

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

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

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

取消

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

throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}

完整实现

function throttle(func, wait, options = {}) {
  let timeout, context, args, previous = 0;

  const later = function() {
      previous = options.leading === false ? 0 : +new Date();
      timeout = null;
      func.apply(context, args);
      if (!timeout) context = args = null; 
  };

  const throttled = function() {
      let now = +new Date();
      if (!previous && options.leading === false) previous = now;
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          previous = now;
          func.apply(context, args);
          if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
          timeout = setTimeout(later, remaining);
      }

      throttled.cancel = function() {
          clearTimeout(timeout);
          previous = 0;
          timeout = null;
      }
  };
  return throttled;
}

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

以上就是利用JavaScript实现防抖节流函数的示例代码的详细内容,更多关于JavaScript防抖节流函数的资料请关注脚本之家其它相关文章!

相关文章

  • javascript利用键盘控制小方块的移动

    javascript利用键盘控制小方块的移动

    这篇文章主要为大家详细介绍了javascript利用键盘控制小方块的移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • 利用javascript实现的三种图片放大镜效果实例(附源码)

    利用javascript实现的三种图片放大镜效果实例(附源码)

    这篇文章主要介绍了利用javascript实现的几种放大镜效果,很实用一款漂亮的js图片放大镜特效,常见于电商网站上产品页,用来放大展示图片细节,很有实用性,推荐下载学习研究。文中提供了完整的源码供大家下载,需要的朋友可以参考借鉴,一起来看看吧。
    2017-01-01
  • 微信小程序block的使用教程

    微信小程序block的使用教程

    这篇文章主要介绍了微信小程序block的使用 ,微信小程序最近非常火热,实现起来也很简单,只要block就可以实现,需要的朋友可以参考下
    2018-04-04
  • JavaScript断言与类型守卫及联合声明超详细介绍

    JavaScript断言与类型守卫及联合声明超详细介绍

    这篇文章主要介绍了JavaScript断言与类型守卫及联合声明,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-11-11
  • JavaScript 原型链学习总结

    JavaScript 原型链学习总结

    在JavaScript中,一切都是对像,函数是第一型
    2010-10-10
  • 一文详解preact的高性能状态管理Signals

    一文详解preact的高性能状态管理Signals

    这篇文章主要介绍了一文详解preact的高性能状态管理Signals,文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的朋友可以参考一下
    2022-09-09
  • JS两种类型的表单提交方法实例分析

    JS两种类型的表单提交方法实例分析

    这篇文章主要介绍了JS两种类型的表单提交方法,结合实例形式分析了2种常用的表单提交验证的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2016-11-11
  • JavaScript实现谷歌浏览器插件开发的方法详解

    JavaScript实现谷歌浏览器插件开发的方法详解

    对于浏览器插件相信大家都不陌生,谁的浏览器不装几个好用的插件呢,更是有油猴这个强大的神器。所以本文就来用JavaScript开发一个谷歌浏览器插件,感兴趣的小伙伴可以了解一下
    2022-11-11
  • 利用Three.js如何实现阴影效果实例代码

    利用Three.js如何实现阴影效果实例代码

    使用three.js可以方便的让我们在网页中做出各种不同的3D效果,下面这篇文章主要给大家介绍了关于利用Three.js如何实现阴影效果的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-09-09
  • 实现点击列表弹出列表索引的两种方式

    实现点击列表弹出列表索引的两种方式

    使用利用事件冒泡委托给列表的父节点去处理的方式第二种方式就是使用闭包了,感兴趣的你可以参考下本文,或许对你学习js有所帮助
    2013-03-03

最新评论