详解防抖Debounce与节流Throttle,入股不亏!

351 阅读8分钟

防抖 Debounce

什么是防抖

如果有一个按钮,每点击一次就发送一个请求,
有时候说不好误触还是无聊,会一直点那个按钮,但是无意义的请求会占用很多资源,我们当然不希望这样。

所谓点击事件的防抖, 就是防止手抖连续点击, 就是指在连续点击中,只执行最后一次点击。
当然也可以不是点击事件,
也可以执行第一次。

官方一点说就是:
防抖,只要函数在一段时间内持续被调用,就不会真的执行这个函数。
该函数将在它停止被调用 wait(wait的值我们可以自己制定) 毫秒后被执行。
如果是立即执行,则函数只在第一次调用之后执行一次。

当然,对于概念性的东西我也说不太好,大家可以多看看别人的解释。

只在最后执行一次的防抖

image.png

立即执行版本的防抖

image.png

纯享版防抖代码

function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        // 定义变量、定义函数
        var context = this, args = arguments;
        var callNow = immediate && !timeout;
        
        // 执行函数
        clearTimeout(timeout);
        timeout = setTimeout(function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        }, wait);
       
        if (callNow) func.apply(context, args);
    };
};


function onMouseMove(){
  console.log('111');
}
var debouncedMouseMove = debounce(onMouseMove, 1000);
window.addEventListener('mousemove', debouncedMouseMove);

史无前例详细注释解释版本

/*
    func:需要被防抖的函数
          可以是一个点击事件,也可以是连续请求事件
    wait:间隔的时间
          函数两次连续触发的间隔大于这个间隔时间,函数才会被执行
    immediate:是否立即执行
               如果是true,那么在连续触发函数中,执行第一次和最后一次
               如果是false,那么只执行最后一次
*/
function debounce(func, wait, immediate) {
  
  /*
      声明一个名为 `timeout` 的变量,我们稍后将使用它来存储 `setTimeout` 函数返回的timeoutID。

        setTimeout 的返回值`timeoutID`是一个正整数,表示定时器的编号。
        这个值可以传递给`clearTimeout()`来取消该定时器。
        在 setTimeout 的第二个参数中传递的指定毫秒数满足之前 调用 `clearTimeout` 时才会阻止 setTimeout 第一个参数里的函数被执行。
  */
  var timeout;

  /*
      通过闭包过程返回一个匿名函数,在这个函数里面可以调用 `debounce` 方法的 `func` 参数。
  */
  return function() {
    /* 
        保存 上下文 和 参数 
        定义不定义都没什么大碍,可能这里只是为了方便看this到底指向谁
    */
    var context = this,    
        args = arguments; 
    // 现在应该调用该函数吗? 如果 immediate 为 true 且 !timeout,则答案是:是
    /*
        什么情况下会 `!timeout === true` 呢 即`!!timeout == false`?
        当定时器id为空的时候,在连续调用函数中的第一次的时候,定时器为空
        callNow 是为了控制第一次是否立即执行
        
        callNow的值:
            true:我们传参的时候,`immediate` 又是 `true`;
                 在定义 `callNow` 的时候,如果是第一次调用函数,那么 timeout 是undefined,!timeout 就是 true。
            false:immediate 传参为 false;
                  或者 在连续调用函数中,不是第一次调用,timeout 保存有只
    */
    var callNow = immediate && !timeout;

    /*
        只要我们的 `debounce` 方法绑定的事件在 `wait` 周期内仍在触发,
        就从 JavaScript 的执行队列中删除 timeoutID(即令`timeout = null`)。
        这可以防止调用 `setTimeout` 函数中传递的函数。

        请记住,`debounce` 方法旨在用于快速触发的事件,即:调整窗口大小或滚动或者连续点击事件。
        事件第一次触发的时候,`timeout` 变量已被声明,但没有分配任何值 - 此时 `timeout === undefined`。
        因此,不会从 JavaScript 的执行队列中删除任何内容,因为队列中没有放置任何内容 - 没有任何内容需要清除。

        如果,`timeout` 变量保存有 `setTimeout`函数返回的 timeoutID。
        只要在 `wait` 时间内连续触发这个函数,
            `timeout` 就会被清除,从而导致在 `setTimeout` 函数中传递的函数从执行队列中删除。
        一旦两次连续触发的时间间隔大于 `wait`,`setTimeout`函数中传递的函数就会执行。
     */
    clearTimeout(timeout);

    // 设置新的计时器
    timeout = setTimeout(function() {
    
      /*
             删除定时器ID。

             注意:通过 setTimeout 执行 later 时,
                  `setTimeout` 函数将返回一个 timeoutID 给 `timeout` 变量。
                  通过将 `null` 分配给 `timeout` 来删除该 timeoutID。
                  
                  我们可以看到后面调用later的地方是这样调用的:
                  `timeout = setTimeout(later, wait);`
                  当两次连续调用在wait时间间隔之内,那么 变量`timeout`必然有值,在later函数里面重置`timeout`的值就是对时间间隔重新计时。
      */
      timeout = null;

      /*
             如果`immediate` 是假的(false),那意思就是只执行最后一次,不是立即执行,
                 那我们就在定时器里面调用函数,这样定时器时间间隔到了,函数才会执行。
      */ 
      if (!immediate) {
        // 使用 apply 调用原始函数
        // apply 允许您定义“this”对象以及参数(均在 setTimeout 之前捕获)
        func.apply(context, args);
      }
    }, wait);

    // 立即模式,没有等待计时器? 执行功能..
    /* 如果是调用之后立即执行,那么就在这里立即执行函数。 */
    /*
        一般来说我们希望在连续事件的最后,满足wait时间之后执行函数,
        但是如果希望在 函数连续反复触发 中的第一次 就执行,那就让`immediate = true`

        如果`immediate = true`,
        则 `callNow` 变量的值将仅在我们的 `debounce` 方法第一次发生事件后才为 `true`。
        
        第一次触发事件后,`timeout` 变量将包含一个false。
        因此,分配给 `callNow` 变量的表达式的结果是 `true`,并且在我们的 `debounce` 方法的 `func` 参数中传递的函数在下面的代码行中执行。

        每次我们的 `debounce` 方法在 `wait` 周期内触发的事件,
        `timeout` 变量保存在前一个事件触发时分配给它的 `setTimout` 函数返回的 timeoutID ,并且`debounce` 方法被执行。

        这意味着对于 `wait` 期间内的所有后续事件,`timeout` 变量保持一个真值,
        并且分配给 `callNow` 变量的表达式的结果是 `false`。
        因此,我们的 `debounce` 方法的 `func` 参数中传递的函数将不会被执行。

        最后,当 `wait` 周期满足并且 `setTimeout` 函数中传递的 `later` 函数执行时,
        结果是它只是将 `null` 分配给 `timeout` 变量。
        在我们的 `debounce` 方法中传递的 `func` 参数将不会被执行,
        因为 `later` 函数中的 `if` 条件失败。
    */
    if (callNow) func.apply(context, args);
  }
}

/////////////////////////////////
// DEMO:

function onMouseMove(){
  console.log('111');
}

// 定义防抖函数
var debouncedMouseMove = debounce(onMouseMove, 50);

// 在每次鼠标移动时调用 debounced 函数
window.addEventListener('mousemove', debouncedMouseMove);

节流 Throttle

什么是节流

image.png image.png image.png

时间戳版本写节流【立即执行】

纯享版

function throttle(func, timeFrame) {
  var lastTime = 0;
  return function (...args) {
      var now = new Date();
      if (now - lastTime >= timeFrame) {
          func(...args);
          lastTime = now;
      }
  };
}

function onMouseMove(){
  console.log('111');
}
var throttleMouseMove = throttle(onMouseMove, 1000);
window.addEventListener('mousemove', throttleMouseMove);

注释版本

function throttle(func, timeFrame) {
  // 定义一个时间,代表最后一次(上一次)触发函数的时间
  var lastTime = 0;
  return function (...args) {
      // 拿到这一次触发函数的时间
      var now = new Date();
      // 如果上一次的时间和这一次的时间间隔 大于或者等于 我们所定义的时间间隔
      // 那么就执行函数
      // 否则就什么也不做
      if (now - lastTime >= timeFrame) {
          func(...args);
          // 更新最后一次触发时间 为 这一次的 触发时间
          lastTime = now;
      }
  };
}

定时器版本写节流【wait时间后再执行(非立即执行】

纯享版

function throttle (fn, delay) {
    var timeout;
    return args => {
        if (timeout) return

        timeout = setTimeout(() => {
            fn.call(this, args)
            clearTimeout(timeout)
            timeout = null
        }, delay)
    }
}



function onMouseMove(){
  console.log('111');
}
var throttleMouseMove = throttle(onMouseMove, 1000);
window.addEventListener('mousemove', throttleMouseMove);

注释版本

function throttle (fn, delay) {
    return args => {
        // 如果 timeout 保存有定时器id,说明不是在这个时间间隔内触发过函数,那么不执行直接返回
        if (timeout) return
        
        // 如果 timeout 没有保存定时器id,说明在这一段时间里面,这是第一次触发函数的,那么进入 定时器, 同时这个定时器的id 保存在 timeout
        timeout = setTimeout(() => {
            /*
                执行函数fn
            */
            fn.call(this, args)
            // 清除定时器。
            /*
                在事件循环中,定时器里面的函数放在任务队列,
                等到调用栈为空的时候才会执行settimeout里面的函数,
                
                如果执行调用栈里面的东西的时间大于settimeout里面的时间间隔,
                就会导致有多个定时器函数积压在任务队列,
                
                等到调用栈为空,执行任务队列的时候,
                就会执行多个积压在任务队列里面的定时器,这样效果很差。
                所以我们把前面的定时器都清除了,只执行最后一个定时器
            */
            clearTimeout(timeout)
            // 清除定时器id----其实并不知道为什么清楚定时器id,是为了垃圾回收吗?
            timeout = null
        }, delay)
    }
}

参考文章

Can someone explain the "debounce" function in Javascript

You don't (may not) need Lodash/Underscore

防抖(Debounce) & 节流(Throttle)

Javascript防抖和节流的深入理解和实践