Underscore源码阅读:throttle, debounce

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,才会执行代码。

想到的额外的拓展

比如说触底刷新,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容