函數節流(throttle)與函數去抖(debounce)

一、前言                                  

  以下場景往往由於事件頻繁被觸發,因而頻繁執行DOM操作、資源加載等重行爲,導致UI停頓甚至瀏覽器崩潰。

  1. window對象的resize、scroll事件

  2. 拖拽時的mousemove事件

  3. 射擊遊戲中的mousedown、keydown事件

  4. 文字輸入、自動完成的keyup事件

  實際上對於window的resize事件,實際需求大多爲停止改變大小n毫秒後執行後續處理;而其他事件大多的需求是以一定的頻率執行後續處理。針對這兩種需求就出現了debounce和throttle兩種解決辦法。

二、什麼是debounce                            

   1. 定義

  如果用手指一直按住一個彈簧,它將不會彈起直到你鬆手爲止。

      也就是說當調用動作n毫秒後,纔會執行該動作,若在這n毫秒內又調用此動作則將重新計算執行時間。

   接口定義

複製代碼
/**
* 空閒控制 返回函數連續調用時,空閒時間必須大於或等於 idle,action 纔會執行
* @param idle   {number}    空閒時間,單位毫秒
* @param action {function}  請求關聯函數,實際應用需要調用的函數
* @return {function}    返回客戶調用函數
*/
debounce(idle,action)
複製代碼

   2. 簡單實現

複製代碼
var debounce = function(idle, action){
  var last
  return function(){
    var ctx = this, args = arguments
    clearTimeout(last)
    last = setTimeout(function(){
        action.apply(ctx, args)
    }, idle)
  }
}
複製代碼

三、什麼是throttle                              

   1. 定義

  如果將水龍頭擰緊直到水是以水滴的形式流出,那你會發現每隔一段時間,就會有一滴水流出。

  也就是會說預先設定一個執行週期,當調用動作的時刻大於等於執行週期則執行該動作,然後進入下一個新週期。

      接口定義:

複製代碼
/**
* 頻率控制 返回函數連續調用時,action 執行頻率限定爲 次 / delay
* @param delay  {number}    延遲時間,單位毫秒
* @param action {function}  請求關聯函數,實際應用需要調用的函數
* @return {function}    返回客戶調用函數
*/
throttle(delay,action)
複製代碼

   2. 簡單實現

複製代碼
var throttle = function(delay, action){
  var last = 0return function(){
    var curr = +new Date()
    if (curr - last > delay){
      action.apply(this, arguments)
last = curr
}
} }
複製代碼

四、underscore v1.7.0相關的源碼剖析                          

   1. _.throttle函數

複製代碼
 _.throttle = function(func, wait, options) {
    /* options的默認值
     *  表示首次調用返回值方法時,會馬上調用func;否則僅會記錄當前時刻,當第二次調用的時間間隔超過wait時,才調用func。
     *  options.leading = true;
     * 表示當調用方法時,未到達wait指定的時間間隔,則啓動計時器延遲調用func函數,若後續在既未達到wait指定的時間間隔和func函數又未被調用的情況下調用返回值方法,則被調用請求將被丟棄。
     *  options.trailing = true; 
     * 注意:當options.trailing = false時,效果與上面的簡單實現效果相同
     */
    var context, args, result;
    var timeout = null;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
      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;
      // 當到達wait指定的時間間隔,則調用func函數
      // 精彩之處:按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,但這裏還考慮到假如客戶端修改了系統時間則馬上執行func函數。
      if (remaining <= 0 || remaining > wait) {
        // 由於setTimeout存在最小時間精度問題,因此會存在到達wait的時間間隔,但之前設置的setTimeout操作還沒被執行,因此爲保險起見,這裏先清理setTimeout操作
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // options.trailing=true時,延時執行func函數
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };
複製代碼
   按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,至於remaining > wait的作用是什麼,我現在也不太清楚。
   精彩之處:按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,但這裏還考慮到假如客戶端修改了系統時間則馬上執行func函數。這裏謝謝@GreatFeng的提示!

 2. _.debounce函數 

複製代碼
_.debounce = function(func, wait, immediate) {
    // immediate默認爲false
    var timeout, args, context, timestamp, result;

    var later = function() {
      // 當wait指定的時間間隔期間多次調用_.debounce返回的函數,則會不斷更新timestamp的值,導致last < wait && last >= 0一直爲true,從而不斷啓動新的計時器延時執行func
      var last = _.now() - timestamp;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      // 第一次調用該方法時,且immediate爲true,則調用func函數
      var callNow = immediate && !timeout;
      // 在wait指定的時間間隔內首次調用該方法,則啓動計時器定時調用func函數
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };
複製代碼

          _.debounce實現的精彩之處我認爲是通過遞歸啓動計時器來代替通過調用clearTimeout來調整調用func函數的延時執行。

五、總結                                  

   throttle和debounce均是通過減少實際邏輯處理過程的執行來提高事件處理函數運行性能的手段,並沒有實質上減少事件的觸發次數。兩者在概念理解上確實比較容易令人混淆,結合各js庫的具體實現進行理解效果將會更好。

   尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/4147810.html ^_^肥子John

六、參考                                  

http://www.alloyteam.com/2012/11/javascript-throttle/

http://www.cnblogs.com/ambar/archive/2011/10/08/throttle-and-debounce.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章