上一篇中講解了Underscore中的去抖函數(_.debounced
),這一篇就來介紹節流函數(_.throttled
)。javascript
通過上一篇文章,我相信不少人都已經瞭解了去抖和節流的概念。去抖,在一段連續的觸發中只能獲得觸發一次的結果,在觸發以後通過一段時間才能夠獲得執行的結果,而且必須在通過這段時間以後,才能夠進入下一個觸發週期。節流不一樣於去抖,節流是一段連續的觸發至少能夠獲得一次觸發結果,上限取決於設置的時間間隔。java
1 理解函數節流
經過這張我手畫的圖,我相信能夠更容易理解函數節流這個概念。git
在這張粗製濫造的手繪圖中,從左往右的軸線表示時間軸,下方的粗藍色線條表示不斷的調用throttled函數(看作連續發生的),而上方的一個一個節點表示咱們獲得的執行func函數的結果。github
從圖上能夠看出來,咱們經過函數節流,成功的限制了func函數在一段時間內的調用頻率,在實際中可以提升咱們應用的性能表現。瀏覽器
接下來咱們探究一下Underscore中_.throttle函數的實現。閉包
2 Underscore的實現
咱們在探究源碼以前,先了解一下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源碼:
- 沒有配置options選項時
- options.leading === false時
- options.trailing === false時
2.1 默認狀況(options === undefined)
在默認狀況下調用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時的狀況。
2.2 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,以後便再也不進行判斷。
2.3 options.trailing === false
此時的區別在於else if中的執行語句。若是options.trailing === false
成立,那麼當remaining>0時間足夠時,不會設置timeout異步任務。那麼如何實現時間到就當即執行func呢?是經過不斷的判斷remaining,一旦remaining <= 0
成立,那麼就當即執行func。
接下來,咱們手動實現一個簡單的throttle函數。
實現一個簡單的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);
更多Underscore源碼閱讀:GitHub