上一篇中講解了Underscore中的去抖函數(_.debounced
),這一篇就來介紹節流函數(_.throttled
)。javascript
通過上一篇文章,我相信不少人都已經瞭解了去抖和節流的概念。去抖,在一段連續的觸發中只能獲得觸發一次的結果,在觸發以後通過一段時間才能夠獲得執行的結果,而且必須在通過這段時間以後,才能夠進入下一個觸發週期。節流不一樣於去抖,節流是一段連續的觸發至少能夠獲得一次觸發結果,上限取決於設置的時間間隔。java
經過這張我手畫的圖,我相信能夠更容易理解函數節流這個概念。git
在這張粗製濫造的手繪圖中,從左往右的軸線表示時間軸,下方的粗藍色線條表示不斷的調用throttled函數(看作連續發生的),而上方的一個一個節點表示咱們獲得的執行func函數的結果。github
從圖上能夠看出來,咱們經過函數節流,成功的限制了func函數在一段時間內的調用頻率,在實際中可以提升咱們應用的性能表現。瀏覽器
接下來咱們探究一下Underscore中_.throttle函數的實現。閉包
咱們在探究源碼以前,先了解一下Underscore API手冊中關於_.throttle函數的使用說明:app
throttle_.throttle(function, wait, [options])異步
建立並返回一個像節流閥同樣的函數,當重複調用函數的時候,最多每隔 wait毫秒調用一次該函數。對於想控制一些觸發頻率較高的事件有幫助。(注:詳見:javascript函數的throttle和debounce)函數
默認狀況下,throttle將在你調用的第一時間儘快執行這個function,而且,若是你在wait週期內調用任意次數的函數,都將盡快的被覆蓋。若是你想禁用第一次首先執行的話,傳遞{leading: false},還有若是你想禁用最後一次執行的話,傳遞{trailing: false}。性能
var throttled = _.throttle(updatePosition, 100);
$(window).scroll(throttled);
結合我畫的那張示意圖,應該比較好理解了。
若是傳遞的options參數中,leading爲false,那麼不會在throttled函數被執行時當即執行func函數;trailing爲false,則不會在結束時調用最後一次func。
Underscore源碼(附註釋):
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function (func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
//previous===0時,下一次會當即觸發。
//previous===_.now()時,下一次不會當即觸發。
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function () {
//獲取當前時間戳(13位milliseconds表示)。
//每一次調用throttled函數,都會從新獲取now,計算時間差。
//而previous只有在func函數被執行事後纔回從新賦值。
//也就是說,每次計算的remaining時間間隔都是每次調用throttled函數與上一次執行func之間的時間差。
var now = _.now();
//!previous確保了在第一次調用時纔會知足條件。
//leading爲false表示不當即執行。
//注意是全等號,只有在傳遞了options參數時,比較纔有意義。
if (!previous && options.leading === false) previous = now;
//計算剩餘時間,now-previous爲已消耗時間。
var remaining = wait - (now - previous);
context = this;
args = arguments;
//remaining <= 0表明當前時間超過了wait時長。
//remaining > wait表明now < previous,這種狀況是不存在的,由於now >= previous是永遠成立的(除非主機時間已經被修改過)。
//此處就至關於只判斷了remaining <= 0是否成立。
if (remaining <= 0 || remaining > wait) {
//防止出現remaining <= 0可是設置的timeout仍然未觸發的狀況。
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
//將要執行func函數,從新設置previous的值,開始下一輪計時。
previous = now;
//時間達到間隔爲wait的要求,當即傳入參數執行func函數。
result = func.apply(context, args);
if (!timeout) context = args = null;
//remaining>0&&remaining<=wait、不忽略最後一個輸出、
//timeout未被設置時,延時調用later並設置timeout。
//若是設置trailing===false,那麼直接跳過延時調用later的部分。
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
複製代碼
接下來,咱們分三種狀況分析Underscore源碼:
在默認狀況下調用throttled函數時,options是一個空的對象{}
,此時options.leading!==false
而且options.trailing!==false
,那麼throttled函數中的第一個if會被忽略掉,由於options.leading === false永遠不會知足。
此時,不斷地調用throttled函數,會按照如下方式執行:
用now變量保存當前調用時的時間戳,previous默認爲0,計算remaining剩餘時間,此時應該會小於0,知足了if (remaining <= 0 || remaining > wait)
。
清空timeout並清除其事件,爲previous從新賦值以記錄當前調用throttled函數的值。
可以進入當前的if語句表示剩餘時間不足或者是第一次調用throttled函數(且options.leading !== false),那麼將會當即執行func函數,使用result記錄執行後的返回值。
下一次調用throttled函數時,從新計算當前時間和剩餘時間,若是剩餘時間不足那麼仍然當即執行func,如此不斷地循環。若是remaining時間足夠(大於0),那麼會進入else if語句,設置一個timeout異步事件,此時注意到timeout會被賦值,直到later被調用纔回被賦值爲null。這樣作的目的就是爲了防止不斷進入else if條件語句重複設置timeout異步事件,影響性能,消耗資源。
以後調用throttled函數,都會按照這樣的方式執行。
經過上面的分析,咱們能夠發現,除非設置options.leading===false,不然第一次執行throttled函數時,條件語句if (!previous && options.leading === false) previous = now;
不會被執行。間接致使remaining<0,而後進入if語句當即執行func函數。
接下來咱們看看設置options.leading === false時的狀況。
設置options.leading爲false時,執行狀況與以前並無太大差別,僅在於if(!previous && options.leading === false)
語句。當options.leading爲false時,第一次執行會知足這個條件,因此賦值previous=== now,間接使得remaining>0。
因爲timeout此時爲undefined,因此!timeout爲true。設置later爲異步任務,在remaining時間以後執行。
此後再不斷的調用throttled方法,思路同2.1無異,由於!previous爲false,因此if(!previous && options.leading === false)
該語句再也不判斷,會被徹底忽略。能夠理解爲設置判斷!previous的目的就是在第一次調用throttled函數時,判斷options.leading是否爲false,以後便再也不進行判斷。
此時的區別在於else if中的執行語句。若是options.trailing === false
成立,那麼當remaining>0時間足夠時,不會設置timeout異步任務。那麼如何實現時間到就當即執行func呢?是經過不斷的判斷remaining,一旦remaining <= 0
成立,那麼就當即執行func。
接下來,咱們手動實現一個簡單的throttle函數。
首先,咱們須要多個throttled函數共享一些變量,好比previous、result、timeout,因此最好的方案仍然是使用閉包,將這些共享的變量做爲throttle函數的私有變量。
其次,咱們須要在返回的函數中不斷地獲取調用該函數時的時間戳now,不斷地計算remaining剩餘時間,爲了實現trailing不等於false時的效果,咱們還須要設置timeout。
最終代碼以下:
var throttle = function(func, wait) {
var timeout, result, now;
var previous = 0;
return function() {
now = +(new Date());
if(now - previous >= wait) {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(this, arguments);
}
else if(!timeout) {
timeout = setTimeout(function() {
previous = now;
result = func.apply(this, arguments);
timeout = null;
}, wait - now + previous);
}
return result;
}
}
複製代碼
可能你們發現了一個問題就是個人now變量也是共享的變量,而underscore中是throttled函數的私有變量,爲何呢?
咱們能夠注意到:underscore設置timeout時,調用的是另一個throttle函數的私有函數,叫作later。later在更新previous的時候,使用的是previous = options.leading === false ? 0 : _.now();
也就是經過_.now
函數直接獲取later被調用時的時間戳。而我使用的是previous = now
,若是now作成throttled的私有變量,那麼timeout的異步任務執行時,設置的previous仍然是過去的時間,而非異步任務被執行時的當前時間。這樣作直接致使的結果就是previous相比實際值更小,remaining會更大,下一次func觸發會來的更早!
下面這段代碼是對上面代碼的應用,你們能夠直接拷貝到瀏覽器的控制檯,回車而後在頁面上滾動鼠標滾輪,看看這個函數實現了怎樣的功能,更有利於你對這篇文章的理解!
var throttle = function(func, wait) {
var timeout, result, now;
var previous = 0;
return function() {
now = +(new Date());
if(now - previous >= wait) {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(this, arguments);
}
else if(!timeout) {
timeout = setTimeout(function() {
previous = now;
result = func.apply(this, arguments);
timeout = null;
}, wait - now + previous);
}
return result;
}
}
window.onscroll = throttle(()=>{console.log('yes')}, 2000);
複製代碼