Skip to content

underscore 函数去抖的实现 #21

Description

@lessfish

前文 我们对 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 https://github.com/jashkenas/underscore/pull/1269)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions