理解Underscore中的節流函數

上一篇中講解了Underscore中的去抖函數(_.debounced),這一篇就來介紹節流函數(_.throttled)。javascript

通過上一篇文章,我相信不少人都已經瞭解了去抖和節流的概念。去抖,在一段連續的觸發中只能獲得觸發一次的結果,在觸發以後通過一段時間才能夠獲得執行的結果,而且必須在通過這段時間以後,才能夠進入下一個觸發週期。節流不一樣於去抖,節流是一段連續的觸發至少能夠獲得一次觸發結果,上限取決於設置的時間間隔。java

1 理解函數節流

經過這張我手畫的圖,我相信能夠更容易理解函數節流這個概念。git

throttle

在這張粗製濫造的手繪圖中,從左往右的軸線表示時間軸,下方的粗藍色線條表示不斷的調用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

相關文章
相關標籤/搜索