Underscore源碼閱讀:throttle, debounce

throttle(func, wait, options)

節(jié)流函數(shù),返回一個函數(shù)的節(jié)流版本;所謂節(jié)流版本,就是給需要執(zhí)行的函數(shù)一個執(zhí)行間隔:每隔waitms才執(zhí)行一次func。
寫個很簡單的節(jié)流函數(shù)還是很簡單的

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變量來控制目標函數(shù)的執(zhí)行,通過定時器來改變flag的值;看上去節(jié)流函數(shù)應該是實現(xiàn)了。但是我們發(fā)現(xiàn),這個節(jié)流函數(shù)默認不會執(zhí)行最后一次執(zhí)行,除非時間卡得好;而且會默認執(zhí)行第一次函數(shù)執(zhí)行,當然這可以通過改變flag初始值來解決。

underscore里對節(jié)流函數(shù)還有個要求,就是options參數(shù):如果你想禁用第一次首先執(zhí)行的話,傳遞{leading: false},還有如果你想禁用最后一次執(zhí)行的話,傳遞{trailing: false}

我們可以通過判斷options.leading來決定flag的初始值;那么如果執(zhí)行最后一次執(zhí)行(這里的最后一次執(zhí)行當然是指函數(shù)執(zhí)行時間發(fā)生在wait期間),該怎么做呢?

通過閱讀underscore源碼,我發(fā)現(xiàn)作者思路是這樣的:被節(jié)流的函數(shù)雖然不會執(zhí)行,卻會生成一個唯一的定時器;這個定時器只會被正確執(zhí)行的函數(shù)銷毀,如果沒有被銷毀,定時器就會執(zhí)行所謂的“最后一次執(zhí)行”。同時后續(xù)的每一二個沒有被正確執(zhí)行的函數(shù),都會更新這個定時器要執(zhí)行的函數(shù)的thisargs

那么思路清晰了,我們來寫這樣一個版本的節(jié)流函數(shù)

var throttle = function (func, wait, options) {
  // 返回函數(shù)的this指針,參數(shù),以及返回結(jié)果
  var result, context, args;
  // 維護定時器
  var timeout = null;
  // 維護上一次函數(shù)執(zhí)行的時間
  var previous = 0;

  options = options || {};

  var later = function () {
    // 這個函數(shù)在某一連續(xù)執(zhí)行階段的最后執(zhí)行的
    // 因此previous需要像一開始那樣初始化
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  }

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

    // 這里是關(guān)鍵,決定了函數(shù)的執(zhí)行與最后一次相關(guān)
    context = this, args = arguments;
    
    if (!previous && options.leading === false) {
      previous = now;
    }
    // 計算下一次觸發(fā)的時間
    var remaining = wait - (now - previous);

    // 已經(jīng)到了觸發(fā)的時間  || 人為修改了時間
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      //記錄這一次代碼執(zhí)行的時間
      previous = now;
      // 執(zhí)行代碼
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 從這里看, 定時器似乎是寫在第二次函數(shù)觸發(fā)的,似乎并不跟最后一次函數(shù)執(zhí)行對應
      // 但實際上,雖然定時器是在第二次函數(shù)觸發(fā),但是其參數(shù)會在最后一次函數(shù)執(zhí)行時重新賦值。
      timeout = setTimeout(later, remaining);
    }
    return result;
  }
}

debounce(func, wait, immediate)

防抖函數(shù)的場景差不多都類似于這兩種:

  1. 當用戶輸入,導致搜索框的內(nèi)容發(fā)生變化時,我們希望函數(shù)的觸發(fā)是在內(nèi)容發(fā)生變化waitms并且沒有變化后才發(fā)出請求
  2. 用戶(更多是測試……)連續(xù)點擊一個button,我們希望只有首次點擊觸發(fā)事件;后waitms內(nèi)的幾次觸發(fā)都不再觸發(fā)事件。

這樣分析,我們發(fā)現(xiàn)防抖函數(shù)的要求有兩種,一種是在waitms的開始觸發(fā),一種是在waitms之后觸發(fā)。這就是debounceimmediate設計的意義。

var debounce = function (func, wait, immediate) {
  var context, result, args;
  // 計時器
  var timeout = null;
  // 記錄上次代碼執(zhí)行時間戳
  var timestamp;

  var later = function () {
    // 計算自上次執(zhí)行代碼過了多久
    var last = _.now() - timestamp;
    if (last < wait && last >= 0) {
      // 如果此時處于代碼執(zhí)行后wait ms內(nèi)
      // 重新設置計時器
      timeout = setTimeout(later, wait - last);
    } else {
      // 此時代碼已經(jīng)執(zhí)行了wait ms
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  }

  return function () {
    // 記錄執(zhí)行狀態(tài)
    context = arguments, context = this;
    // 記錄執(zhí)行時間戳
    timestamp = _.now();
    // 代碼是否立即執(zhí)行
    var canNow = immediate && !timeout;
    if (canNow) {
      result = func.apply(context, args);
      context = args = null;
    }
    // 設置定時器
    if (!timeout) {
      time = setTimeout(later, wait);
    }
  }
}

如果immediatetrue,canNow變量就會生效,第一次執(zhí)行就會執(zhí)行代碼;并且在之后的計時器里,只要計時器存在,代碼就不會被執(zhí)行。
如果immediatefalse,每次執(zhí)行防抖函數(shù)都會更新時間戳;定時器自設置waitms后就會比較當前時間和時間戳,如果相差waitms,才會執(zhí)行代碼。

想到的額外的拓展

比如說觸底刷新,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容