何爲去抖函數?在學習Underscore去抖函數以前咱們須要先弄明白這個概念。不少人都會把去抖跟節流兩個概念弄混,可是這兩個概念實際上是很好理解的。javascript
去抖函數(Debounce Function),是一個能夠限制指定函數觸發頻率的函數。咱們能夠理解爲連續調用同一個函數屢次,只獲得執行該函數一次的結果;可是隔一段時間再次調用時,又能夠從新得到新的結果,具體這段時間有多長取決於咱們的設置。這種函數的應用場景有哪些呢?java
好比咱們寫一個DOM事件監聽函數,git
window.onscroll = function(){
console.log('Got it!');
}
複製代碼
如今當咱們滑動鼠標滾輪的時候,咱們就能夠看到事件被觸發了。可是咱們能夠發如今咱們滾動鼠標滾輪的時候,咱們的控制檯在不斷的打印消息,由於window的scroll事件被咱們不斷的觸發了。github
在當前場景下,可能這是一個無傷大雅的行爲,可是能夠預見到,當咱們的事件監聽函數(Event Handler)涉及到一些複雜的操做時(好比Ajax請求、DOM渲染、大量數據計算),會對計算機性能產生多大影響;在一些比較老舊的機型或者較低版本的瀏覽器(尤爲IE)中,極可能會致使死機狀況的出現。因此這個時候咱們就要想辦法,在指定時間段內,只執行必定次數的事件處理函數。瀏覽器
說了一些概念和應用場景,可是仍是很拗口,到底什麼是去抖函數?閉包
咱們能夠經過以下實例來理解:app
假設有如下代碼:異步
//本身實現的簡單演示代碼,未實現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函數就是我本身實現的一個簡單的去抖函數,咱們能夠經過這段代碼進行實驗。函數
步驟以下:性能
經過以上步驟,咱們能夠發現當咱們連續滾動鼠標時,控制檯沒有消息被打印出來,中止2s之內並再次滾動時,也沒有消息輸出;可是當咱們中止的時間超過2s時,咱們能夠看到控制檯有消息輸出。
這就是去抖函數。在連續的觸發中(不管時長),只能獲得觸發一次的效果。在指定時間長度內連續觸發,最多隻能獲得一次觸發的效果。
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對應的異步代碼就會被執行。