throttle
與debounce
,是兩個算法。兩者處理的對象,都是那些需要被反復調用的函數,特別是回調函數。其目的,都是控制函數重複執行的頻率,避免性能下降。
throttle
的應用場景如瀏覽器窗口的resize,debounce
的應用場景如搜索框的suggestion。
throttle
的基本思想很簡單,通過封裝(包裹),使原函數相鄰兩次執行時間必須大於一個值interval
。如果第二次調用函數的時機,沒有超出interval
,則忽略之,或者用setTimeout
使函數在超出interval
的某個時機再執行原函數。
debounce
的思路也很簡單,通過封裝(包裹),使函數第一次調用時,不馬上執行原函數,而是設置一個timeout
。如果在timeout
等待過程中,繼續調用這個函數,則取消現在的timeout
,并重新設置一個。
但是underscore庫中的這兩個函數,加了一些參數,使得邏輯非常複雜。即使每個函數的核心代碼就20行左右,我也要花兩三天時間才能理順。
throttle
_.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;
//remaining > wait,在人為將系統時間調慢的情況下,成立
if (remaining <= 0 || remaining > wait) {
//由於setTimeout存在最小時間精度問題,因此有可能wait階段已過,但之前設置的setTimeout操作還未執行
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;
};
};
該函數的第三個參數options
,有兩個屬性leading
和trailing
,都是布爾值。他們能形成四種組合形式:
{"trailing": true,"leading": true}
,是為默認值{"trailing": true,"leading": false}
{"trailing": false,"leading": true}
{"trailing": false,"leading": false}
這個參數會影響程序的流程,現以流程圖分述之。
下為{"trailing": true,"leading": true}
的情況:
下為{"trailing": true,"leading": false}
的情況:
下為{"trailing": false,"leading": true}
的情況:
下為{"trailing": false,"leading": false}
的情況:
leading
和trailing
這兩個科技術語,一般是用來表示「首」和「尾」。但是通過上面流程圖的演示,可知在這段程序中,他們各自代表的意義,並非典型的前與後的對立關係。外國程序員在用詞方面的瑕疵,也給我們的閱讀造成了一定的困擾。
debounce
_.debounce = function(func, wait, immediate) {// immediate默認為false
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;//如果在wait期間,函數被調用,導致timestamp被更新,則原函數始終不會被執行,直到函數停止被調用
if (last < wait && last >= 0) {//系統時間調慢之後,則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();//此處是重點,每次都更新timestamp,這個值會影響到後面later函數的執行
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {//第一次調用該函數,或者上一個timeout已經被執行完畢,且immediate為true,則立即執行func函數
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
immediate
這個參數,目的在於,在不存在timeout
的情況下,先同步執行一次原函數,然後再設置一個timeout
。
以下是流程圖:
測試
除了上面的源代碼,我也在自己的Javascript庫中實現了這兩個函數,沒有那麼多的參數,邏輯更加清楚。
為了測試這兩個函數,我寫了一段簡單的代碼:
var todo = function() {
var now = new Date();
console.log(now.getSeconds() + "," + now.getMilliseconds());
};
var callback = debounce(todo, 1230); //事件的響應函數,參數1230ms
var interval = setInterval(function() {
callback();
}, 100); //事件每100ms觸發一次
setTimeout(function() {
clearTimeout(interval);
}, 6 * 1000); //事件持續6s