淺談 Underscorejs中 _.throttle 和 _.debounce 的差別和使用場景

一般的函數(或方法)調用過程分爲三個部分:請求、執行和響應。(文中「請求」與「調用」同義,「響應」與「返回」同義,爲了更好的表述,刻意採用請求和響應的說法。)css

某些場景下,好比響應鼠標移動或者窗口大小調整的事件,觸發頻率比較高。若稍處理函數微複雜,須要較多的運算執行時間,響應速度跟不上觸發頻率,每每會出現延遲,致使假死或者卡頓感。ajax

在運算資源不夠的時候,最直觀的解決辦法就是升級硬件,誠然經過購買更好的硬件能夠解決部分問題,可是也須要爲此付出高額的成本。特別是客戶端和服務器模式,要求客戶端統一升級硬件基本不可能。服務器

在資源有限的前提下,處理函數沒法即時響應高頻調用。退而求其次,只響應部分請求是否可行呢?某些場景下的密集性請求,具有很強的同質和連續性。好比說,鼠標移動的軌跡參數。響應越及時效果越平滑,可是若是響應速度跟不上時,反而會出現卡頓感,若是適當的丟棄一些請求效果更流暢。app

throttledebounce 是解決請求和響應速度不匹配問題的兩個方案。兩者的差別在於選擇不一樣的策略。函數

電梯超時this

想象天天上班大廈底下的電梯。把電梯完成一次運送,類比爲一次函數的執行和響應。假設電梯有兩種運行策略 throttledebounce ,超時設定爲15秒,不考慮容量限制。code

  • throttle 策略的電梯。保證若是電梯第一我的進來後,15秒後準時運送一次,不等待。若是沒有人,則待機。對象

  • debounce 策略的電梯。若是電梯裏有人進來,等待15秒。若是又人進來,15秒等待從新計時,直到15秒超時,開始運送。遊戲

使用示例事件

_.throttle 使用示例

function log( event ) {
  console.log( $(window).scrollTop(), event.timeStamp );
};

// 控制檯記錄窗口滾動事件,觸發頻率比你想象的要快
$(window).scroll( log );

// 控制檯記錄窗口滾動事件,每250ms最多觸發一次
$(window).scroll( _.throttle( log, 250 ) );

_.debounce 使用示例

function ajax_lookup( event ) {
  // 對輸入的內容$(this).val()執行 Ajax 查詢
};

// 字符輸入的頻率比你預想的要快,Ajax 請求來不及回覆。
$('input:text').keyup( ajax_lookup );

// 當用戶停頓250毫秒之後纔開始查找
$('input:text').keyup( _.debounce( ajax_lookup, 250 ) );


underscore源碼註解

讓咱們來讀讀源碼,探其究竟。基於開發版本(1.7.0)的源碼,加上了一些註釋以幫助理解。

_.throttle 方法源碼

/**
 * 頻率控制 返回函數連續調用時,func 執行頻率限定爲 次 / wait
 * 
 * @param  {function}   func      傳入函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   若是想忽略開始邊界上的調用,傳入{leading: false}。
 *                                若是想忽略結尾邊界上的調用,傳入{trailing: false}
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 上次執行時間點
  var previous = 0;
  if (!options) options = {};
  // 延遲執行函數
  var later = function() {
    // 若設定了開始邊界不執行選項,上次執行時間始終爲0
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = _.now();
    // 首次執行時,若是設定了開始邊界不執行選項,將上次執行時間設定爲當前時間。
    if (!previous && options.leading === false) previous = now;
    // 延遲執行時間間隔
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 延遲時間間隔remaining小於等於0,表示上次執行至此所間隔時間已經超過一個時間窗口
    // remaining大於時間窗口wait,表示客戶端系統時間被調整過
    if (remaining <= 0 || remaining > wait) {
      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 方法源碼

/**
 * 空閒控制 返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        傳入函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,調用觸發於開始邊界而不是結束邊界
 * @return {function}             返回客戶調用函數
 */
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result;

  var later = function() {
    // 據上一次觸發時間間隔
    var last = _.now() - timestamp;

    // 上次被包裝函數被調用時間間隔last小於設定時間間隔wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      // 若是設定爲immediate===true,由於開始邊界已經調用過了此處無需調用
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function() {
    context = this;
    args = arguments;
    timestamp = _.now();
    var callNow = immediate && !timeout;
    // 若是延時不存在,從新設定延時
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
};


使用場景

只要牽涉到連續事件或頻率控制相關的應用均可以考慮到這兩個函數,好比:

  • 遊戲射擊,keydown 事件

  • 文本輸入、自動完成,keyup 事件

  • 鼠標移動,mousemove 事件

  • DOM 元素動態定位,window 對象的 resize 和 scroll 事件

前二者 debouncethrottle 均可以按需使用;後二者確定是用 throttle 了。若是不作過濾處理,每秒種甚至會觸發數十次相應的事件。尤爲是 mousemove 事件,每移動一像素均可能觸發一次事件。若是是在一個畫布上作一個鼠標相關的應用,過濾事件處理是必須的,不然確定會形成糟糕的體驗。


附錄:別人封裝的獨立版http://www.css88.com/archives/4648

/*
* 頻率控制 返回函數連續調用時,fn 執行頻率限定爲每多少時間執行一次
* @param fn {function}  須要調用的函數
* @param delay  {number}    延遲時間,單位毫秒
* @param immediate  {bool} 給 immediate參數傳遞false 綁定的函數先執行,而不是delay後後執行。
* @return {function}實際調用函數
*/
var throttle = function (fn,delay, immediate, debounce) {
   var curr = +new Date(),//當前事件
       last_call = 0,
       last_exec = 0,
       timer = null,
       diff, //時間差
       context,//上下文
       args,
       exec = function () {
           last_exec = curr;
           fn.apply(context, args);
       };
   return function () {
       curr= +new Date();
       context = this,
       args = arguments,
       diff = curr - (debounce ? last_call : last_exec) - delay;
       clearTimeout(timer);
       if (debounce) {
           if (immediate) {
               timer = setTimeout(exec, delay);
           } else if (diff >= 0) {
               exec();
           }
       } else {
           if (diff >= 0) {
               exec();
           } else if (immediate) {
               timer = setTimeout(exec, -diff);
           }
       }
       last_call = curr;
   }
};
 
/*
* 空閒控制 返回函數連續調用時,空閒時間必須大於或等於 delay,fn 纔會執行
* @param fn {function}  要調用的函數
* @param delay   {number}    空閒時間
* @param immediate  {bool} 給 immediate參數傳遞false 綁定的函數先執行,而不是delay後後執行。
* @return {function}實際調用函數
*/
 
var debounce = function (fn, delay, immediate) {
   return throttle(fn, delay, immediate, true);
};
相關文章
相關標籤/搜索