試議常用Javascript 類庫中 throttle 與 debounce 輔助函數的區別


問題的引出

看過我前面兩篇博客的童鞋可能會注意到都談到了事件處理的優化問題。

在很多應用中,我們需要控制函數執行的頻率,

例如 窗口的 resize,窗口的 scroll 等操作,事件觸發的頻率非常高,如果處理函數比較復雜,需要較多的計算時間,那么會加重瀏覽器的負擔,這時我們很自然會想到:能否在不影響顯示效果(對顯示效果影響在可接受范圍內)的前提下減少事件響應函數的執行頻率呢?

朴素的解決思路

  1. 首先我們會想到設置一定的時間范圍delay,每隔delay ms 執行不超過一次。
    事件處理函數什么時候執行能? 這里有兩個選擇,一是先執行,再間隔delay ms來等待;或者是先等待delay ms,然后執行事件處理函數。

  2. 操作過程中的事件全不管,反正只執行一次事件處理。
    相同低,這一次的事件處理可以是先執行一次,然后后面的事件都不管; 或者前面的都不管,最后操作完了再執行一次事件處理。

當然上面是簡單的通俗描述。

throttle 與 debounce 函數現狀

throttle與debounce即是對上面兩種思路的具體實現。

在現在很多的javascript框架中都提供了這兩個函數。例如 jquery中有throttle和debounce插件, underscore.js ,Lo-dash.js 等都提供了這兩個函數。

釋義

throttle
在英文中是節流閥的意思,顧名思義,就像原本一直流水的龍頭,現在每隔一定時間開一下,以節流。

*下圖 “|” 表示throttle函數的一次執行, x 表示事件處理函數fn的一次執行*

|||||||||||||||||||||||| (暫停) ||||||||||||||||||||||||

X           X           X        X           X           X

debounce
bounce是名詞彈力,或者動詞反彈的意思, de-表否定/相反的前綴,形象低表示一直持續低壓抑住不讓反彈,當然最后還是得松手,那么反彈一次。

|||||||||||||||||||||||| (暫停) ||||||||||||||||||||||||  

X                       X        X                       X

*上圖 “|” 表示debounce函數的一次執行, x 表示事件處理函數fn的一次執行*

區別

throttle 可以想象成閥門一樣定時打開來調節流量。 debounce可以想象成把很多事件壓縮組合成了一個事件。

有個生動的類比,假設你在乘電梯,沒次進一個人需等待10秒鍾,不考慮電梯容量限制,那么兩種不同的策略是:

謝謝 wonyun ,已改正:

debounce 你在進入電梯后發現這時不遠處走來了了一個人,等10秒鍾,這個人進電梯后不遠處又有個妹紙姍姍來遲,怎么辦,再等10秒,於是妹紙上電梯時又來了一對好基友...,作為感動中國好碼農,你要每進一個人就等10秒,直到沒有人進來,10秒超時,電梯開動。

throttle 電梯很忙,每次就只等10秒,不管是來了妹紙還是好基友,電梯每隔10秒准時送一波人。

因此,簡單來說,debounce適合只執行一次的情況,例如 搜索框中的自動完成。在停止輸入后提交一次ajax請求;
而throttle適合指定每隔一定時間間隔內執行不超過一次的情況,例如拖動滾動條,移動鼠標的事件處理等。

簡單代碼實現及實驗結果

那么下面我們自己簡單地實現下這兩個函數:

throttle 函數:

  window.addEventListener("resize", throttle(callback, 300, {leading:false}));
  window.addEventListener("resize", callback2);
  function callback ()  { console.count("Throttled"); }
  function callback2 () { console.count("Not Throttled"); }
 /**
  * 頻率控制函數, fn執行次數不超過 1 次/delay
  * @param fn{Function}     傳入的函數
  * @param delay{Number}    時間間隔
  * @param options{Object}  如果想忽略開始邊界上的調用則傳入 {leading:false},
  *                         如果想忽略結束邊界上的調用則傳入 {trailing:false},
  * @returns {Function}     返回調用函數
  */
 function throttle(fn,delay,options) {
     var wait=false;
     if (!options) options = {};
     return function(){
         var that = this,args=arguments;
         if(!wait){
             if (!(options.leading === false)){
                 fn.apply(that,args);
             }
             wait=true;
             setTimeout(function () {
                 if (!(options.trailing === false)){
                     fn.apply(that,args);
                 }
                 wait=false;
             },delay);
         }
     }
 }

將以上代碼貼入瀏覽器中運行,可得到:

下面再看debounce函數的情況,
debounce 函數:

window.addEventListener("resize", throttle(callback, 300, {leading:false}));
window.addEventListener("resize", callback2);
function callback ()  { console.count("Throttled"); }
function callback2 () { console.count("Not Throttled"); }
/**
 * 空閑控制函數, fn僅執行一次
 * @param fn{Function}     傳入的函數
 * @param delay{Number}    時間間隔
 * @param options{Object}  如果想忽略開始邊界上的調用則傳入 {leading:false},
 *                         如果想忽略結束邊界上的調用則傳入 {trailing:false},
 * @returns {Function}     返回調用函數
 */
function debounce(fn, delay, options) {
    var timeoutId;
    if (!options) options = {};
    var leadingExc = false;

    return function() {
        var that = this,
            args = arguments;
        if (!leadingExc&&!(options.leading === false)) {
            fn.apply(that, args);
        }
        leadingExc=true;
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        timeoutId = setTimeout(function() {
            if (!(options.trailing === false)) {
                fn.apply(that, args);
            }
            leadingExc=false;
        }, delay);
    }
}

將以上代碼貼入瀏覽器中運行,分三次改變窗口大小,可看到,每一次改變窗口的大小都會把開始和結束邊界的事件處理函數各執行一次:

如果是一次性改變窗口大小,會發現開始和結束的邊界各執行一次時間處理函數,請注意與一次性改變窗口大小時 throttle 情況的對比:

underscore.js 的代碼實現

_.throttle函數

 /**
  * 頻率控制函數, fn執行次數不超過 1 次/delay
  * @param fn{Function}     傳入的函數
  * @param delay{Number}    時間間隔
  * @param options{Object}  如果想忽略開始邊界上的調用則傳入 {leading:false},
  *                         如果想忽略結束邊界上的調用則傳入 {trailing:false},
  * @returns {Function}     返回調用函數
  */
_.throttle = function(func, wait, options) {
    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;
        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函數

/**
 * 空閑控制函數, fn僅執行一次
 * @param fn{Function}     傳入的函數
 * @param delay{Number}    時間間隔
 * @param options{Object}  如果想忽略開始邊界上的調用則傳入 {leading:false},
 *                         如果想忽略結束邊界上的調用則傳入 {trailing:false},
 * @returns {Function}     返回調用函數
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
        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();
        var callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }
        return result;
    };
};   

###參考鏈接###: [Debounce and Throttle: a visual explanation](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) [jQuery throttle / debounce: Sometimes, less is more!](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) [underscore.js](http://underscorejs.org/)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM