防抖与节流函数

128 阅读4分钟

前言

防抖和节流函数在开发的过程中时常都会用到,它也是函数式编程的一个比较经典的应用,在开发的过程中我们总是免不了要给元素绑定像scroll、mousemove、change等事件监听并执行其相关的回调函数执行相应的函数逻辑。

然而我们往往希望该函数相应的函数逻辑不要太频繁反复的执行,而是能够减少执行的频率。那么使用防抖或者节流函数是一个很好的选择。

假设我们给一个div元素一个mousemove事件,其html代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Debounce</title>
  <style type="text/css">
    #box {
      width: 400px;
      height: 200px;
      line-height: 200px;
      text-align: center;
      margin: 0 auto;
      background-color: lightgreen;
      color: #fff;
    }
  </style>
</head>
<body>
  <div id="box"></div>
  <script type="text/javascript">
    let num = 0;
    const box = document.getElementById('box');
    box.innerHTML = num;
    function count() {
      box.innerHTML = num++;
    };
    box.addEventListener('mousemove',count,false);
  </script>
  </body>
</html>

效果如下动图所示,鼠标在该div元素中进行频繁的移动就会使事件被频繁地触发,从而导致回调函数被频繁的触发,div元素中的数字会疯狂的增长。

未防抖.gif

如果我们采用防抖或者节流函数,使该回调函数作为函数参数传递给防抖或节流函数,然后防抖或者节流函数返回一个新的函数作为新的回调函数,可以使原来的count函数不会频繁的执行

防抖和节流的效果区别

防抖(debounce)是无论你触发多少次事件,只要相邻两次触发的间隔时间小于你设定间隔时间,就会重新设定间隔时间,如果一直以这样的频率触发,最后函数只会被执行一次

节流(throttle)是连续触发事件的过程中以一定时间间隔执行函数。 比如你用了10秒触发了100次事件,你的间隔时间是1秒,那事实上你触发了100次事件,但是执行了10次,每间隔1秒钟,只会执行一次操作,无论这1秒钟内触发了多少次事件。

防抖(debounce)

简单一句话概括就是,触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 分为:非立即执行版和立即执行版。

非立即执行版:

function debounce(fn, delay){
    let t; //用于接收setTimeout返回的标识;
    return function() {
        let _this = this,
            _arguments = arguments; //arguments中有事件对象
        clearTimeout(t);
        t = setTimeout(function(){
            fn.apply(_this, arguments);
        },delay);
    }
}

box.addEventListener("mousemove", debounce(count, 1000), "false");

非立即执行版的效果如下,即触发事件之后函数并不会立即执行,会在n秒之后才执行,而如果在n秒内又对事件进行触发,就会重新延迟函数的执行时间。

防抖.gif

可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。

而立即执行版的代码如下所示:

function debounce(fn, delay){

    let t; //用于接收setTimeout返回的标识;
    
    return function() {
    
      let firstTime = !t;
      
      if(firstTime){
        fn.apply(this, arguments);
      }

      clearTimeout(t);
      
      t = setTimeout(() => {
        t = null;
      }, delay);
    }
}

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

效果如下:

立即执行版防抖.gif

由于有不立即执行版和立即执行版的存在,我们可以将这两个版本进行合并处理,用第三个参数来决定要执行哪种代码逻辑,代码如下:

 function debounce(fn, delay, i){ //i为true则立即执行,i为false则不立即执行
    let t;
    return function () {
      let _this = this;
      let args = [...arguments];
      if(t){clearTimeout(t)};
      if(i){
        let first = !t;
        t = setTimeout( () => {
          t = null;
        }, delay);

        if(first){
          fn.apply(_this, args);
        }
      }else{
        t = setTimeout( () => {
          fn.apply(_this, args);
        }, delay)
      }
    }
}

节流(throttle)

节流的效果就是如果n秒中连续地触发事件,则n秒过后只会执行一次函数,也就是说节流是会稀释函数的执行频率的。 节流一般可以分为时间戳版还有延时器版本。

节流时间戳版

代码如下:

function throttle(fn, delay){
    let lastTime = 0;
    return function () {
        let nowTime = +new Date();
        if(nowTime - lastTime > delay){
          fn.apply(this, arguments);
          lastTime = nowTime;
        }
    }
}

时间戳版效果

时间戳版.gif

节流延时器版

代码如下:

     function throttle(fn, delay){
          let t;
          return function () {
            if(!t){
              t = setTimeout( () => {
                t = null;
                fn.apply(this, arguments);
              }, delay);
            }
          }
    }

延时器版本的效果

延时器版.gif

时间戳版和延时器版结合

时间戳版本和延时器版本的区别,如果我们仔细对比两个效果图会发现,时间戳版是在每个时间段的开头就会执行一次,而延时器版本就是在每个时间段的结尾才执行一次。当然这也有结合版本,如果想第一次立即执行,之后是固定间隔时间执行,可以使用这个结合版本。 结合版本的代码如下:

 function throttle(fn, delay){
      let t,
          lastTime = 0;

      let which_throttle = function () {
        let _this = this,
            _arguments = arguments,
            nowTime = +new Date(),
            interval_time = nowTime - lastTime,
            remain_time = delay - interval_time,
            later = function () {
              t = null;
              fn.apply(_this, _arguments);
              lastTime = nowTime;
            };
            console.log(interval_time);

            if(t){
              clearTimeout(t);
              t = null;
            }

            if(interval_time > delay){
              fn.apply(_this, _arguments);
              lastTime = nowTime;
            }else{
              t = setTimeout(function () {
                later();
              }, remain_time);
            }
      }

      return which_throttle;
    }

结合版本效果

节流结合.gif

参考文章: juejin.cn/post/684490… github.com/mqyqingfeng…