从underscore库中学习debounce

731 阅读4分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

上文防抖简介中简单介绍了一下防抖的使用场景以及一个简单点实现的例子, 支持一般的需求,它存在一些使用方面的限制,譬如

  • 执行函数的this 指向问题
  • 执行函数的参数获取问题
  • 不能立即执行

有没有功能丰富的防抖?

有! 它来了 本文将解答一个高配版本的防抖underscore库中的防抖函数

先介绍一下本章节实例背景:有一个div,在div上移动触发回调函数listenMoveOn,这里使用了简单版本的防抖,如下代码所示

<style>
    div {
        background-color: #666;
        height: 300px;
    }
</style><div></div><script>
    let dom = document.getElementsByTagName("div")[0];
    dom.onmousemove = debounce(listenMoveOn, 200);
​
    function listenMoveOn() {
        // 监听的回调做一些事情 
    }
    /**
     *
     * @param {function} fn - 回调函数
     * @param {number} delay - 等待时间
     */
    function debounce(fn, delay) {
        var timer = null;
        return function (...args) {
            clearTimeout(timer);
            timer = setTimeout(fn, delay);
        };
    }
</script>

this指向问题

这里listenMoveOn中打印的this指向window,原因在于listenMoveOn是通过setTimeout调用的,还记得这是谁的方法吗?window.setTimeout,就是它window!!!所以listenMoveOn中的this指向window

由于onmousemove监听的回调是debounce函数体返回的那个函数,所以这个函数体内的this是指向这个dom元素的

function listenMoveOn() {
    // this -> window
}
function debounce(fn, delay) {
    ...
    return function () {
        // this -> dom
    };
}

既然事情已经明了,那么改变listenMoveOn函数体中的this指向应该不是难事了吧?如下使用apply

function listenMoveOn() {
    // this -> dom                  
}
function debounce(fn, delay) {
    ...
    return function () {
        // this -> dom
        ...
        timer = setTimeout(() => {
            fn.apply(this)          // 在这里改变listenMoveOn的this指向
        }, delay);      
    };
}

传参问题

这个问题其实可以和this指向一起 解决,但是还是想多说两句,因为我监听鼠标移动的事件是debounce函数体返回的那个函数,js事件通常都会返回一个事件对象,我目前只能在debounce中获取,在事件回调listenMoveOn中是获取不到的,解决办法如下

function listenMoveOn(...args) {
    let [event] = args
    // args可以获取到事件监听返回的事件对象
    // this -> dom                  
}
function debounce(fn, delay) {
    ...
    return function (...args) {
        // this -> dom
        ...
        timer = setTimeout(() => {
            fn.apply(this, args)    // 在这里改变listenMoveOn的this指向的同时传入监听对象获取到的参数,当然使用arguments也可以
        }, delay);      
    };
}

立即执行

这个实现起来有点复杂,对于underscore中的debounce源码我也稍加改变,功能实现原理一样,具体可以看代码注释,基本每一行都有注释!!(因为这个实现完就整体实现了,所以注释的比较仔细)

function listenMoveOn(...args) {
  // 监听的回调做一些事情
  // this -> dom
  // args -> [event]
}
​
/**
 *
 * @param {function} fn - 回调函数
 * @param {number} delay - 等待时间
 * @param {boolean} immediate - 是否立即执行
 */
function debounce(fn, delay, immediate = false) {
  /**
    *
    * @param timer - 定时器
    * @param timestamp - 当前时间
    * @param context - 保存上下文使用
    * @param param - 保存事件参数使用
    */
  let timer, timestamp, context, param;
​
  const later = function() {
    let last = +new Date() - timestamp  // 距离上一次操作的时间间隔ms
    if(last < delay && last >= 0) {
        timer = setTimeout(later, delay - last) // 重置定时器
    } else {  // 进入 else 说明设置的delay时间已经到了
        timer = null   // 清除闭包中的保存的变量,并且如果有后续的话,可以让立即调用中的callNow更改为true,进行调用事件回调函数
        if (!immediate) {
            fn.apply(context, param)   // 如果immediate = false需要调用一下事件处理函数;当immediate = true 时,因为立即调用过,所以在后面就不需要调用,在本段代码 倒数第五行 执行立即调用
            context = param = null  // 清除闭包中的保存的变量
        }
    }
  }
  
  // return的这个函数才是鼠标事件监听的函数哈,鼠标只要在div上一移动就会触发这里
  return function (...args) {
    context = this  // 保存变量到闭包内,方便后面调用时拿到值
    param = args
    timestamp = +new Date()
      
    let callNow = immediate && !timer   // 当 immediate 设置true,并且timer还没有被赋值时,说明刚进来,可进行立刻调用,下面一句话就会重新赋值timer
    if (!timer) timer = setTimeout(later, delay)    // 当第一次进入设置timer
    if (callNow) {                      
        fn.apply(context, args)         // 立即执行的调用在这里
        context = param = null          // 清除闭包中的保存的变量
    }
  };
}

underscore中的debounce

最后可以欣赏一下underscore中的debounce源码


// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
  _.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;
​
    var later = function() {
      var last = _.now() - timestamp;
​
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };
​
    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      var callNow = immediate && !timeout;
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }
​
      return result;
    };
  };

本文源码地址

本章小结

如有不对之处,欢迎指正 ✍