美文网首页
Underscore源码阅读:throttle, debounc

Underscore源码阅读:throttle, debounc

作者: San十二 | 来源:发表于2018-11-15 20:40 被阅读0次

throttle(func, wait, options)

节流函数,返回一个函数的节流版本;所谓节流版本,就是给需要执行的函数一个执行间隔:每隔waitms才执行一次func
写个很简单的节流函数还是很简单的

var throttle = function (func, wait) {
  var context, args;
  var flag = true;
  var later = function () {
    flag = true;
  }

  return function () {
    var result;
    var context = this, args = arguments;
    if (flag) {
      result = func.apply(context, arguments);
      flag = false;
      setTimeout(later, wait);
    }
    return result;
  }
}

我们用一个flag变量来控制目标函数的执行,通过定时器来改变flag的值;看上去节流函数应该是实现了。但是我们发现,这个节流函数默认不会执行最后一次执行,除非时间卡得好;而且会默认执行第一次函数执行,当然这可以通过改变flag初始值来解决。

underscore里对节流函数还有个要求,就是options参数:如果你想禁用第一次首先执行的话,传递{leading: false},还有如果你想禁用最后一次执行的话,传递{trailing: false}

我们可以通过判断options.leading来决定flag的初始值;那么如果执行最后一次执行(这里的最后一次执行当然是指函数执行时间发生在wait期间),该怎么做呢?

通过阅读underscore源码,我发现作者思路是这样的:被节流的函数虽然不会执行,却会生成一个唯一的定时器;这个定时器只会被正确执行的函数销毁,如果没有被销毁,定时器就会执行所谓的“最后一次执行”。同时后续的每一二个没有被正确执行的函数,都会更新这个定时器要执行的函数的thisargs

那么思路清晰了,我们来写这样一个版本的节流函数

var throttle = function (func, wait, options) {
  // 返回函数的this指针,参数,以及返回结果
  var result, context, args;
  // 维护定时器
  var timeout = null;
  // 维护上一次函数执行的时间
  var previous = 0;

  options = options || {};

  var later = function () {
    // 这个函数在某一连续执行阶段的最后执行的
    // 因此previous需要像一开始那样初始化
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  }

  return function () {
    var now = _.now();

    // 这里是关键,决定了函数的执行与最后一次相关
    context = this, args = arguments;
    
    if (!previous && options.leading === false) {
      previous = now;
    }
    // 计算下一次触发的时间
    var remaining = wait - (now - previous);

    // 已经到了触发的时间  || 人为修改了时间
    if (remaining <= 0 || remaining > wait) {
      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);
    }
    return result;
  }
}

debounce(func, wait, immediate)

防抖函数的场景差不多都类似于这两种:

  1. 当用户输入,导致搜索框的内容发生变化时,我们希望函数的触发是在内容发生变化waitms并且没有变化后才发出请求
  2. 用户(更多是测试……)连续点击一个button,我们希望只有首次点击触发事件;后waitms内的几次触发都不再触发事件。

这样分析,我们发现防抖函数的要求有两种,一种是在waitms的开始触发,一种是在waitms之后触发。这就是debounceimmediate设计的意义。

var debounce = function (func, wait, immediate) {
  var context, result, args;
  // 计时器
  var timeout = null;
  // 记录上次代码执行时间戳
  var timestamp;

  var later = function () {
    // 计算自上次执行代码过了多久
    var last = _.now() - timestamp;
    if (last < wait && last >= 0) {
      // 如果此时处于代码执行后wait ms内
      // 重新设置计时器
      timeout = setTimeout(later, wait - last);
    } else {
      // 此时代码已经执行了wait ms
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  }

  return function () {
    // 记录执行状态
    context = arguments, context = this;
    // 记录执行时间戳
    timestamp = _.now();
    // 代码是否立即执行
    var canNow = immediate && !timeout;
    if (canNow) {
      result = func.apply(context, args);
      context = args = null;
    }
    // 设置定时器
    if (!timeout) {
      time = setTimeout(later, wait);
    }
  }
}

如果immediatetruecanNow变量就会生效,第一次执行就会执行代码;并且在之后的计时器里,只要计时器存在,代码就不会被执行。
如果immediatefalse,每次执行防抖函数都会更新时间戳;定时器自设置waitms后就会比较当前时间和时间戳,如果相差waitms,才会执行代码。

想到的额外的拓展

比如说触底刷新,

相关文章

网友评论

      本文标题:Underscore源码阅读:throttle, debounc

      本文链接:https://www.haomeiwen.com/subject/tousfqtx.html