underscore 函数去抖的实现 #21

前文 我们对 JavaScript 中的函数节流和函数去抖的概念和应用场景进行了简单的了解,本文我们来深入探究下函数去抖的实现。(不懂函数去抖概念的建议看下前文 JavaScript 函数节流和函数去抖应用场景辨析 )

我们以 scroll 事件为例,探究如何实现滚动一次窗口打印一个 hello world 字符串。

如果不对其进行节流或者去抖控制:

window.onscroll = function() {
  console.log('hello world');
};

这样每滚动一次,实际上会打印 N 多个 hello world。函数去抖背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

《高程三》给出了最简洁最经典的去抖代码(书中说是节流,实则为去抖),调用如下:

function debounce(method, context) {
  clearTimeout(method.tId);
  method.tId = setTimeout(function() {
    method.call(context);
  }, 1000);
}

function print() {
  console.log('hello world');
}

window.onscroll = function() {
  debounce(print);
};

在窗口内滚动一次,停止,1000ms 后,打印了 hello world,因为我们设置了一个 1000ms 延迟的定时器,细思非常巧妙。

underscore 在其基础上进行了扩充,直接看代码,含大量注释:

// 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.
// 函数去抖(连续事件触发结束后只触发一次)
// sample 1: _.debounce(function(){}, 1000)
// 连续事件结束后的 1000ms 后触发
// sample 1: _.debounce(function(){}, 1000, true)
// 连续事件触发后立即触发(此时会忽略第二个参数)
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result;

  var later = function() {
    // 定时器设置的回调 later 方法的触发时间,和连续事件触发的最后一次时间戳的间隔
    // 如果间隔为 wait(或者刚好大于 wait),则触发事件
    var last = _.now() - timestamp;

    // 时间间隔 last 在 [0, wait) 中
    // 还没到触发的点,则继续设置定时器
    // last 值应该不会小于 0 吧?
    if (last < wait && last >= 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      // 到了可以触发的时间点
      timeout = null;
      // 可以触发了
      // 并且不是设置为立即触发的
      // 因为如果是立即触发(callNow),也会进入这个回调中
      // 主要是为了将 timeout 值置为空,使之不影响下次连续事件的触发
      // 如果不是立即执行,随即执行 func 方法
      if (!immediate) {
        // 执行 func 函数
        result = func.apply(context, args);
        // 这里的 timeout 一定是 null 了吧
        // 感觉这个判断多余了
        if (!timeout)
          context = args = null;
      }
    }
  };

  // 嗯,闭包返回的函数,是可以传入参数的
  return function() {
    // 可以指定 this 指向
    context = this;
    args = arguments;

    // 每次触发函数,更新时间戳
    // later 方法中取 last 值时用到该变量
    // 判断距离上次触发事件是否已经过了 wait seconds 了
    // 即我们需要距离最后一次触发事件 wait seconds 后触发这个回调方法
    timestamp = _.now();

    // 立即触发需要满足两个条件
    // immediate 参数为 true,并且 timeout 还没设置
    // immediate 参数为 true 是显而易见的
    // 如果去掉 !timeout 的条件,就会一直触发,而不是触发一次
    // 因为第一次触发后已经设置了 timeout,所以根据 timeout 是否为空可以判断是否是首次触发
    var callNow = immediate && !timeout;

    // 设置 wait seconds 后触发 later 方法
    // 无论是否 callNow(如果是 callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数)
    // 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
    if (!timeout)
      // 设置了 timeout,所以以后不会进入这个 if 分支了
      timeout = setTimeout(later, wait);

    // 如果是立即触发
    if (callNow) {
      // func 可能是有返回值的
      result = func.apply(context, args);
      // 解除引用
      context = args = null;
    }

    return result;
  };
};

等等,一下子多了这么多代码,那么我们比基础版多了哪些功能(优势)呢?

首先,基础版能做的,我们一样能做,一样让它在连续滚动后停止的 1000ms 后打印 hello world

function print() {
  console.log('hello world');
}

window.onscroll = _.debounce(print, 1000);

我们还可以在滚动刚触发的时候打印字符串,而不是连续滚动结束后,只需传入第三个参数,会自动忽略第二个参数:

function print() {
  console.log('hello world');
}

window.onscroll = _.debounce(print, 1000, true);

这样对于连续的滚动,也只会打印一次,但是是在事件第一次触发的时候。

回调函数需要传入参数?一点问题都没有。

function print(a) {
  console.log('The passed item is: ' + a);
}

var callback = _.debounce(print, 1000);
window.onscroll = function() {
  var item = 'zichi';
  callback(item);
};

当然,除了功能上的优势,性能也是提高不少,最显而易见的是基础版每此触发事件都会取消定时器,然后重新设置定时器,而 underscore 中会在一定时间后才取消定时器,重新设置定时器。其他更多可以细究下源码。(对性能有兴趣的可以看看这个 pr jashkenas/underscore#1269)

你可能感兴趣的:(underscore 函数去抖的实现 #21)