理解Underscore中的去抖函式

Russ_Zhong發表於2018-03-05

何為去抖函式?在學習Underscore去抖函式之前我們需要先弄明白這個概念。很多人都會把去抖跟節流兩個概念弄混,但是這兩個概念其實是很好理解的。

去抖函式(Debounce Function),是一個可以限制指定函式觸發頻率的函式。我們可以理解為連續呼叫同一個函式多次,只得到執行該函式一次的結果;但是隔一段時間再次呼叫時,又可以重新獲得新的結果,具體這段時間有多長取決於我們的設定。這種函式的應用場景有哪些呢?

比如我們寫一個DOM事件監聽函式,

window.onscroll = function(){
    console.log('Got it!');
}
複製程式碼

現在當我們滑動滑鼠滾輪的時候,我們就可以看到事件被觸發了。但是我們可以發現在我們滾動滑鼠滾輪的時候,我們的控制檯在不斷的列印訊息,因為window的scroll事件被我們不斷的觸發了。

在當前場景下,可能這是一個無傷大雅的行為,但是可以預見到,當我們的事件監聽函式(Event Handler)涉及到一些複雜的操作時(比如Ajax請求、DOM渲染、大量資料計算),會對計算機效能產生多大影響;在一些比較老舊的機型或者較低版本的瀏覽器(尤其IE)中,很可能會導致當機情況的出現。所以這個時候我們就要想辦法,在指定時間段內,只執行一定次數的事件處理函式。

理解去抖函式

說了一些概念和應用場景,但是還是很拗口,到底什麼是去抖函式?

我們可以通過如下例項來理解:

假設有以下程式碼:

//自己實現的簡單演示程式碼,未實現immediate功能,歡迎改進。
var debounce = function (callback, delay, immediate) {
	var timeout, result;
	return function () {
		var callNow;
		if (timeout)
			clearTimeout(timeout);
		callNow = !timeout && immediate;
		if (callNow) {
			result = callback.apply(this, Array.prototype.slice.call(arguments, 0));
			timeout = {};
		}
		else {
			timeout = setTimeout(() => {
				callback.apply(this, Array.prototype.slice.call(arguments, 0));
			}, delay);
		}
	};
};
var s = debounce(() => {
	console.log('yes...');
}, 2000);
window.onscroll = s;
複製程式碼

debounce函式就是我自己實現的一個簡單的去抖函式,我們可以通過這段程式碼進行實驗。

步驟如下:

  • 複製以上程式碼,開啟瀏覽器,開啟控制檯(F12),然後貼上程式碼並回車執行。
  • 連續不斷的滾動滑鼠,檢視控制檯有無輸出。
  • 停止滾動滑鼠,2s之內再次滾動滑鼠,檢視是否有輸出。
  • 連續滾動之後停止2s以上,檢視是否有輸出。

通過以上步驟,我們可以發現當我們連續滾動滑鼠時,控制檯沒有訊息被列印出來,停止2s以內並再次滾動時,也沒有訊息輸出;但是當我們停止的時間超過2s時,我們可以看到控制檯有訊息輸出。

這就是去抖函式。在連續的觸發中(無論時長),只能得到觸發一次的效果。在指定時間長度內連續觸發,最多隻能得到一次觸發的效果。

underscore的實現

underscore原始碼如下(附程式碼註釋):

// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
//去抖函式,傳入的函式在wait時間之後(或之前)執行,並且只會被執行一次。
//如果immediate傳遞為true,那麼在函式被傳遞時就立即呼叫。
//實現原理:涉及到非同步JavaScript,多次呼叫_.debounce返回的函式,會一次性執行完,但是每次呼叫
//該函式又會清空上一次的TimeoutID,所以實際上只執行了最後一個setTimeout的內容。
_.debounce = function (func, wait, immediate) {
	var timeout, result;

	var later = function (context, args) {
		timeout = null;
		//如果沒有傳遞args引數,那麼func不執行。
		if (args) result = func.apply(context, args);
	};

	//被返回的函式,該函式只會被呼叫一次。
	var debounced = restArgs(function (args) {
		//這行程式碼的作用是清除上一次的TimeoutID,
		//使得如果有多次呼叫該函式的場景時,只執行最後一次呼叫的延時。
		if (timeout) clearTimeout(timeout);
		if (immediate) {
			////如果傳遞了immediate並且timeout為空,那麼就立即呼叫func,否則不立即呼叫。
			var callNow = !timeout;
			//下面這行程式碼,later函式內部的func函式註定不會被執行,因為沒有給later傳遞引數。
			//它的作用是確保返回了一個timeout,並且保持到wait毫秒之後,才執行later,
			//清空timeout。而清空timeout是在immediate為true時,callNow為true的條件。
			//timeout = setTimeout(later, wait)的存在是既保證上升沿觸發,
			//又保證wait內最多觸發一次的必要條件。
			timeout = setTimeout(later, wait);
			if (callNow) result = func.apply(this, args);
		} else {
			//如果沒有傳遞immediate,那麼就使用_.delay函式延時執行later。
			timeout = _.delay(later, wait, this, args);
		}

		return result;
	});

	//該函式用於取消當前去抖效果。
	debounced.cancel = function () {
		clearTimeout(timeout);
		timeout = null;
	};

	return debounced;
};
複製程式碼

可以看到underscore使用了閉包的方法,定義了兩個私有屬性:timeout和result,以及兩個私有方法later和debounced。最終會返回debounced作為處理之後的函式。timeout用於接受並儲存setTimeout返回的TimeoutID,result用於執行使用者傳入的func函式的執行結果,later方法用於執行傳入的func函式。

實現原理

利用了JavaScript的非同步執行機制,JavaScript會優先執行完所有的同步程式碼,然後去事件佇列中執行所有的非同步任務。

當我們不斷的觸發debounced函式時,它會不斷的clearTimeout(timeout),然後再重新設定新的timeout,所以實際上在我們的同步程式碼執行完之前,每次呼叫debounced函式都會重置timeout。所以非同步事件佇列中的非同步任務會不斷重新整理,直到最後一個debounced函式執行完。只有最後一個debounced函式設定的later非同步任務會在同步程式碼執行之後被執行。

所以當我們在之前實驗中不斷的滾動滑鼠時,實際上是在不斷的呼叫debounced函式,不斷的清除timeout對應的非同步任務,然後又設定新的timeout非同步任務。當我們停止的時間不超過2s時,timeout對應的非同步任務還沒有被觸發,所以再次滾動滑鼠觸發debounced函式還可以清除timeout任務然後設定新的timeout任務。一旦停止的時間超過2s,最終的timeout對應的非同步程式碼就會被執行。

總結

  • 去抖是限制函式執行頻率的一種方法。
  • 去抖後的函式在指定時間內最多被觸發一次,連續觸發去抖後的函式只能得到一次的觸發效果。
  • underscore去抖的實現依賴於JavaScript的非同步執行機制,優先執行同步程式碼,然後執行事件佇列中的非同步程式碼。

參考

相關文章