Underscore 源码学习 (五) - Throttle 和 Debounce

Underscore 的函数大部分还是挺好理解的,感觉过一遍就行了,不过今天看到两个函数感觉还是挺有意思的,并且也挺常用。这两个函数就是 throttle 和 debounce。

throttle

_.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    // 更改previous即上一次执行时间为当前时间
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = _.now();
    // 如果leading为false时禁用第一次首先执行,previous等于now(效果同已经执行过一次,所以第一次被禁用)
    // 这个if语句只在第一次执行该函数的时候有效
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 超时处理和未到时的处理
    if (remaining <= 0 || remaining > wait) {
      // timeout不为null时清除掉并设置为null
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      // 立即调用
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {    // 如果没有禁用最后一次执行
      timeout = setTimeout(later, remaining);               // remaining毫秒后执行later
    }
    // 返回调用的结果
    return result;
  };

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

  return throttled;
};

其实一步步去理解也不难,就不细说这个方法了。这个方法有可选项设置,分别为 {leading: false} 和 {trailing: false}。

所以一般有四类情况:

  • 默认情况
    第一次调用时立即响应,之后每个周期内最多执行一次,周期内触发会产生定时执行使在上一次执行时间 preview 周期时间后再次执行。
  • 设置 leadingfalse
    同默认情况区别在于第一次调用不会立即执行而是等待周期时间后再次执行,如果在周期时间内触发,一样等待上一次执行时间 preview 周期时间后再执行。
  • 设置 trailingfalse
    最后周期内最多执行一次,但在周期时间内调用不会触发 timeout,只能在上一次 timeout 失效后调用才能生效并且此时调用将立即执行。
  • 设置 leadingtrailingfalse
    如果同时还设置 leadingfalse 的话,那么第一次调用不会立即执行而是等待周期时间后才执行,在这段时间内调用都不会有效果。

比如

var func = _.throttle(updatePosition, 100);
$(window).scroll(func);

由于 scroll 过程时, func 函数的调用是很密集的,我们不能每次调用都去执行,可以通过设置 throttle 来达到节流阀的作用。leadingtrailing 只是实现上细微的不同而已。

throll 主要应用在鼠标移动,mousemove 事件,DOM 元素动态定位,window 对象的 resize 和 scroll 等事件。这些事件触发频率高,但又要尽可能进行响应。

debounce

_.debounce = function(func, wait, immediate) {
  var timeout, result;

  var later = function(context, args) {
    timeout = null;
    if (args) result = func.apply(context, args);
  };

  var debounced = restArgs(function(args) {
    // 再次调用且上次还未执行,则清除上次的timeout
    // 只是timeout事件不再执行,但timeout依旧存在
    if (timeout) clearTimeout(timeout);
    // 如果immediate为true
    if (immediate) {
      // 如果timeout为null,则立即调用函数
      // 如果timeout不为null,则callNow为false,函数不执行
      var callNow = !timeout;
      timeout = setTimeout(later, wait);
      if (callNow) result = func.apply(this, args);
    } else {
      // 延迟later执行,如果这个还没到时间再来一次,则新的会覆盖上一次的
      timeout = _.delay(later, wait, this, args);
    }

    return result;
  });

  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
};

这个函数就要比上面那个好理解一点,说明下这个函数:

  • 如果 immediatetrue,周期 100ms

    • 0 => 立即执行,设置 timeout
    • 50 => 重新设置 timeout,不执行
    • 100 => 重新设置 timeout,不执行
    • 200 => timeout 到时,执行函数
  • 如果 immediatefalse, 周期 100ms

    • 0 => 设置 timeout,不执行
    • 100 => timeout 到时,执行函数
    • 120 => 设置 timeout,不执行
    • 180 => 重新设置 timeout,不执行
    • 280 => timeout 到时,执行函数

其实 timeout 就变成一个控制两次事件触发间隔用的,并且和上面的 throttle 不同,timeout 会被重新设置。

debounce 主要应用在文本输入 keydown 事件,keyup 事件,例如做 autocomplete。

结合两个的应用细细体味下他们差别。如果想自己体验下差别,传送门