在看 underscore.js 源碼的時候,接觸到了這樣兩個方法,很有意思:
我先把實現的代碼撂在下面,看不懂的可以先跳過,但是跳過可不是永遠跳過哦~
一個是 throttle:
1 _.throttle = function(func, wait, options) { 2 var context, args, result; 3 4 // setTimeout 的 handler 5 var timeout = null; 6 7 // 標記時間戳 8 // 上一次執行回調的時間戳 9 var previous = 0; 10 11 // 如果沒有傳入 options 參數 12 // 則將 options 參數置為空對象 13 if (!options) 14 options = {}; 15 16 var later = function() { 17 // 如果 options.leading === false 18 // 則每次觸發回調后將 previous 置為 0 19 // 否則置為當前時間戳 20 previous = options.leading === false ? 0 : _.now(); 21 timeout = null; 22 result = func.apply(context, args); 23 // 這里的 timeout 變量一定是 null 了吧 24 // 是否沒有必要進行判斷? 25 if (!timeout) 26 context = args = null; 27 }; 28 29 // 以滾輪事件為例(scroll) 30 // 每次觸發滾輪事件即執行這個返回的方法 31 // _.throttle 方法返回的函數 32 return function() { 33 // 記錄當前時間戳 34 var now = _.now(); 35 36 // 第一次執行回調(此時 previous 為 0,之后 previous 值為上一次時間戳) 37 // 並且如果程序設定第一個回調不是立即執行的(options.leading === false) 38 // 則將 previous 值(表示上次執行的時間戳)設為 now 的時間戳(第一次觸發時) 39 // 表示剛執行過,這次就不用執行了 40 if (!previous && options.leading === false) 41 previous = now; 42 43 // 距離下次觸發 func 還需要等待的時間 44 var remaining = wait - (now - previous); 45 context = this; 46 args = arguments; 47 48 // 要么是到了間隔時間了,隨即觸發方法(remaining <= 0) 49 // 要么是沒有傳入 {leading: false},且第一次觸發回調,即立即觸發 50 // 此時 previous 為 0,wait - (now - previous) 也滿足 <= 0 51 // 之后便會把 previous 值迅速置為 now 52 // ========= // 53 // remaining > wait,表示客戶端系統時間被調整過 54 // 則馬上執行 func 函數 55 // @see https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs 56 if (remaining <= 0 || remaining > wait) { 57 if (timeout) { 58 clearTimeout(timeout); 59 // 解除引用,防止內存泄露 60 timeout = null; 61 } 62 63 // 重置前一次觸發的時間戳 64 previous = now; 65 66 // 觸發方法 67 // result 為該方法返回值 68 result = func.apply(context, args); 69 // 引用置為空,防止內存泄露 70 // 感覺這里的 timeout 肯定是 null 啊?這個 if 判斷沒必要吧? 71 if (!timeout) 72 context = args = null; 73 } else if (!timeout && options.trailing !== false) { // 最后一次需要觸發的情況 74 // 如果已經存在一個定時器,則不會進入該 if 分支 75 // 如果 {trailing: false},即最后一次不需要觸發了,也不會進入這個分支 76 // 間隔 remaining milliseconds 后觸發 later 方法 77 timeout = setTimeout(later, remaining); 78 } 79 80 // 回調返回值 81 return result; 82 }; 83 };
一個是debounce:
1 _.debounce = function(func, wait, immediate) { 2 var timeout, args, context, timestamp, result; 3 4 var later = function() { 5 // 定時器設置的回調 later 方法的觸發時間,和連續事件觸發的最后一次時間戳的間隔 6 // 如果間隔為 wait(或者剛好大於 wait),則觸發事件 7 var last = _.now() - timestamp; 8 9 // 時間間隔 last 在 [0, wait) 中 10 // 還沒到觸發的點,則繼續設置定時器 11 // last 值應該不會小於 0 吧? 12 if (last < wait && last >= 0) { 13 timeout = setTimeout(later, wait - last); 14 } else { 15 // 到了可以觸發的時間點 16 timeout = null; 17 // 可以觸發了 18 // 並且不是設置為立即觸發的 19 // 因為如果是立即觸發(callNow),也會進入這個回調中 20 // 主要是為了將 timeout 值置為空,使之不影響下次連續事件的觸發 21 // 如果不是立即執行,隨即執行 func 方法 22 if (!immediate) { 23 // 執行 func 函數 24 result = func.apply(context, args); 25 // 這里的 timeout 一定是 null 了吧 26 // 感覺這個判斷多余了 27 if (!timeout) 28 context = args = null; 29 } 30 } 31 }; 32 33 // 嗯,閉包返回的函數,是可以傳入參數的 34 // 也是 DOM 事件所觸發的回調函數 35 return function() { 36 // 可以指定 this 指向 37 context = this; 38 args = arguments; 39 40 // 每次觸發函數,更新時間戳 41 // later 方法中取 last 值時用到該變量 42 // 判斷距離上次觸發事件是否已經過了 wait seconds 了 43 // 即我們需要距離最后一次事件觸發 wait seconds 后觸發這個回調方法 44 timestamp = _.now(); 45 46 // 立即觸發需要滿足兩個條件 47 // immediate 參數為 true,並且 timeout 還沒設置 48 // immediate 參數為 true 是顯而易見的 49 // 如果去掉 !timeout 的條件,就會一直觸發,而不是觸發一次 50 // 因為第一次觸發后已經設置了 timeout,所以根據 timeout 是否為空可以判斷是否是首次觸發 51 var callNow = immediate && !timeout; 52 53 // 設置 wait seconds 后觸發 later 方法 54 // 無論是否 callNow(如果是 callNow,也進入 later 方法,去 later 方法中判斷是否執行相應回調函數) 55 // 在某一段的連續觸發中,只會在第一次觸發時進入這個 if 分支中 56 if (!timeout) 57 // 設置了 timeout,所以以后不會進入這個 if 分支了 58 timeout = setTimeout(later, wait); 59 60 // 如果是立即觸發 61 if (callNow) { 62 // func 可能是有返回值的 63 result = func.apply(context, args); 64 // 解除引用 65 context = args = null; 66 } 67 68 return result; 69 }; 70 };
在開發過程中,經常會遇到處理頻率很高的事件或着連續的事件,比如像在 window 的 resize/scroll 事件調用某個事件 A 等,如果不進行處理,這個時候伴隨着resize就會執行無數個 事件A ,若事件 A 是個很復雜的函數,需要較多的運算執行時間,響應速度跟不上觸發頻率,往往會出現延遲,導致假死或者卡頓感,並且很多情況下並不是需要“如此”誇張的頻繁調用事件 A。
下面 gif 錄制了一個頻繁調用 事件 A 的例子:
1 $(window).on("resize",function(){ 2 console.log("A"); // 事件A 3 })
我們可以想辦法來解決這樣的問題,一種方式就是規定一個時間間隔 wait ,每隔固定 wait 固定執行一次函數 A,也就是我們要說的 throttle 方法,另一種方式則是在連續事件結束之后開始 過一個時間間隔 wait 執行一次函數 A,也就是我們要說的 debounce方法。
針對例子 resize 來解釋( 假設時間間隔5s ):
throttle 方法:在 resize 的過程中,每隔5s執行一次函數 A;
debounce方法:在 resize 結束之后,過了 5s ,執行一次函數 A,如果沒有到 5s ,就又開始 resize ,那么就重新計時,並不執行函數 A;
如果我的這個解釋沒有理解,那么很有一個很多人用的電梯的例子與生活結合在一起會更加形象,在這里也引用一下:
想象每天上班大廈底下的電梯。把電梯完成一次運送,類比為一次函數的執行和響應。假設電梯有兩種運行方法 throttle
和 debounce
,超時設定為15秒,不考慮容量限制。
throttle
方法的電梯。保證如果電梯第一個人進來后,15秒后准時運送一次,不等待。如果沒有人,則待機。debounce
方法的電梯。如果電梯里有人進來,等待15秒。如果又人進來,15秒等待重新計時,直到15秒超時,開始運送。
下面就逐一說一下兩個方法:
1、 throttle 方法:
Underscore.js 中 針對這個方法 傳入三個參數:func 即你要在密集事件內調用的函數,也就是例子中的 事件 A,wait 即時間間隔,而第三個參數 options 可以傳兩種選項,一種是 {leading: false},表示你想禁用第一次首先執行函數 A,如果不傳,就表示你想上來先調用一次 函數 A,另一種是 {trailing: false},表示想禁用最后一次執行函數 A ;
(此圖來自 http://benalman.com/projects/jquery-throttle-debounce-plugin/)
2、debounce 方法
Underscore.js 中 針對這個方法 傳入三個參數:func 即你要在密集事件內調用的函數,也就是例子中的 事件 A,wait 即時間間隔,而第三個參數 immediate 可以傳 true 或者 false。傳為 true, debounce 會在 wait 時間間隔的開始調用這個函數 (注:並且在 waite 的時間之內,不會再次調用)。在類似不小心點了提交按鈕兩下而提交了兩次的情況下很有用。
(此圖來自 http://benalman.com/projects/jquery-throttle-debounce-plugin/,圖中 at_begain 即可看為 immediate )
兩者使用方法類似:
// WRONG 錯誤的調用方法 $(window).on('resize', function() { _.throttle(doSomething, 300); }); // RIGHT 正確的調用方法 $(window).on('resize', _.throttle(doSomething, 300));
最后,兩者既然有區別,那么也有針對不同場景下更好的選擇方法:
throttle:
1、DOM 元素動態定位,window 對象的 resize 和 scroll 事件,比如:用戶在你無限滾動的頁面上向下拖動,你需要判斷現在距離頁面底部多少。如果用戶快接近底部時,我們應該發送請求來加載更多內容到頁面。
2、如果用戶在 30s 內 input 輸入非常塊,但你想固定每間隔 5s 就進行一次某個事情。
debounce:
1、AutoComplete中的Ajax請求使用的keypress
2、在進行input校驗的時候,“你的密碼太短”等類似的信息。
其實選擇哪個方法主要取決於你是否想確保在固定的時間間隔進行回調。
別忘了再回頭研究一下最上方的源碼。
嘿嘿,贊和關注哦~ ε-(´∀`; )
參考並感謝:
https://segmentfault.com/a/1190000004909376 里面有例子,大家可以進去試試
http://benalman.com/projects/jquery-throttle-debounce-plugin/
https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs
http://drupalmotion.com/article/debounce-and-throttle-visual-explanation