淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異
Underscore.js是一個很精幹的庫,壓縮後只有5.2KB。它提供了幾十種函數式編程的方法,彌補了標準庫的不足,大大方便了JavaScript的編程。
本文僅探討Underscore.js的兩個函數方法
_.throttle
和 _.debounce
的原理、效果和用途。
通常的函數(或方法)調用過程分為三個部分:請求、執行和響應。(文中“請求”與“調用”同義,“響應”與“返回”同義,為了更好的表述,刻意採用請求和響應的說法。)
某些場景下,比如響應鼠標移動或者窗口大小調整的事件,觸發頻率比較高。若稍處理函數微複雜,需要較多的運算執行時間,響應速度跟不上觸發頻率,往往會出現延遲,導致假死或者卡頓感。
在運算資源不夠的時候,最直觀的解決辦法就是升級硬件,誠然通過購買更好的硬件可以解決部分問題,但是也需要為此付出高額的成本。特別是客戶端和服務器模式,要求客戶端統一升級硬件基本不可能。
在資源有限的前提下,處理函數無法即時響應高頻調用。退而求其次,只響應部分請求是否可行呢?某些場景下的密集性請求,具備很強的同質和連續性。比如說,鼠標移動的軌跡參數。響應越及時效果越平滑,但是如果響應速度跟不上時,反而會出現卡頓感,如果適當的丟棄一些請求效果更流暢。
throttle
和 debounce
是解決請求和響應速度不匹配問題的兩個方案。二者的差異在於選擇不同的策略。電梯超時
想像每天上班大廈底下的電梯。把電梯完成一次運送,類比為一次函數的執行和響應。假設電梯有兩種運行策略
throttle
和 debounce
,超時設定為15秒,不考慮容量限制。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;
};
};
可視化演示
示例中每一行都以30ms的速度繪製時間軸,第一行Mousemove Events是參考基準,以50ms每次的響應頻率,在時間軸上輸出循環可見ASCII碼字符。
當鼠標進入左側方型區域(mouseenter 事件)所有行開始繪製時間軸, 鼠標晃動(mousemove 事件)會在時間軸上繪製字符塊,每個字符塊表示事件被觸發一次。為了展現延遲觸發效果,相鄰字符塊的演示和文字是不同的。
頂部的兩個按鈕
每100毫秒觸發1次
和每200毫秒觸發2次
演示以固定頻率勻速觸發事件的效果。使用場景
只要牽涉到連續事件或頻率控制相關的應用都可以考慮到這兩個函數,比如:
- 遊戲射擊,keydown 事件
- 文本輸入、自動完成,keyup 事件
- 鼠標移動,mousemove 事件
- DOM 元素動態定位,window 對象的 resize 和 scroll 事件
前兩者 debounce 和 throttle 都可以按需使用;後兩者肯定是用 throttle 了。如果不做過濾處理,每秒種甚至會觸發數十次相應的事件。尤其是 mousemove 事件,每移動一像素都可能觸發一次事件。如果是在一個畫布上做一個鼠標相關的應用,過濾事件處理是必須的,否則肯定會造成糟糕的體驗。
參考閱讀
- UNDERSCORE.JS
- 高階函數 debounce 和 throttle
- jQuery throttle / debounce: Sometimes, less is more!
- Debounce and Throttle: a visual explanation
資料來源:https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs