節流(throttle)
和防抖(debounce)
是網頁性能優化的手段之一,也是你們在開發過程當中常常忽視的點。面試也常常會被問,同時是前端進階重要的知識點。
本文從概述,實現和源碼三個部分着手,由淺入深的給你們分析和講解節流和防抖的原理及實現,使讀者可以明白其中原理並可以手寫出相關代碼。javascript
在解釋防抖(debounce)和節流(throttle)以前,咱們來看一下下面的例子 html
此處是被我改造過的百度,咱們看到一旦咱們輸入,控制檯就同步輸出。此處有一個細節,一開始使用keydown觸發時,input中的value爲空,也就是此時尚未輸入任何信息前端
核心代碼java
let search = document.getElementById("kw");
search.addEventListener('keydown',function(){
console.log(node.value)
})
複製代碼
一旦用戶輸入(keydown
),百度就是根據請求查詢相關詞條。若是咱們對接二連三輸入,首先下降前端的性能,輸入過快或者網速過慢就會出現延遲請求卡頓,增長後端服務器的壓力。
如今使用underscore的防抖函數,來看一下加入防抖以後的效果 node
防抖——讓輸入框更智能化,在用戶輸入完成超過必定時間才輸出結果面試
核心代碼後端
window.onload = function(){
function print(){
console.log(node.value);
}
var _print = _.debounce(print, 600)
let node = document.getElementById("kw");
node.addEventListener('keydown',_print);
}
複製代碼
直接使用underscore的工具函數debounce
,第一個參數是你要觸發的內容,第二個參數根據官方解釋:性能優化
postpone its execution until after wait milliseconds have elapsed since the last time服務器
也就是說閉包
延遲最後執行的時間
wait
毫秒。在這個例子中,就是指你鍵盤一直不停的輸入,若是兩次輸入間隔時間大於600ms,執行函數
防抖用於延遲執行最後的動做。
節流的目的和防抖同樣,但有略微區別,根據underscore官網解釋的區別以下
when invoked repeatedly, will only actually call the original function at most once per every waitmilliseconds
翻譯出來就是
當重複調用的時候,真正觸發的只是最開始的函數,並且觸發這個函數的等待時間最可能是
wait
毫秒。
什麼意思?若是使用 var _print = _.throttle(print, 1000)
,那麼若是用戶在百度中接二連三的輸入數據時,從鍵入開始,每1s鍾就會觸發一次打印事件,以下所示:
節流——在接二連三輸入時,咱們看到節流頗有規律,每1s打印一次。
核心代碼
window.onload = function(){
function print(){
console.log(node.value)
}
var _print = _.throttle(print, 1000)
let node = document.getElementById("kw");
node.addEventListener('keydown',_print);
}
複製代碼
咱們再來舉個栗子🌰,若是咱們把百度接受用戶請求比做站臺載客問題,在政府沒有管理(沒用防抖和節流)以前,在站臺上一旦來了乘客就上了出租車開走了。人一多,車就不少,交通擁擠(服務器壓力變大),這個時候政府說我要來介入管理(節流和防抖),政府規定接客用大客車,而且制定了兩條規則:
這裏邊的規則1就是節流(第一我的說了算)
,規則2就是防抖(最後一人說了算)
。這兩種方式都可以減輕交通壓力。
在scroll 事件,resize 事件、鼠標事件(好比 mousemove、mouseover 等)、鍵盤事件(keyup、keydown 等)存在被頻繁觸發的回調時間當中,使用throttle(事件節流)和 debounce(事件防抖)可以提升前端的性能,減小服務器壓力。
上述其實已經把節流和防抖的概念,做用和區別,下面咱們根據原理來進行代碼實現
如概述中,節流就是「第一我的說了算」。在上述例子中,當在百度搜索框中,第一次按下鍵盤,就開始計時,等待「必定時間」後執行,而在這段時間內的觸發事件直接被「節流閥」屏蔽掉。根據這個思想能夠大體寫一個節流函數。
// fn是咱們須要包裝的事件回調, interval是時間間隔的閾值
Function.prototype.throttle = (fn, interval)=>{
// last爲上一次觸發回調的時間
let last = 0;
// 將throttle處理結果看成函數返回
return function () {
// 保留調用時的this上下文
let context = this
// 保留調用時傳入的參數
let args = arguments
// 記錄本次觸發回調的時間
let now = +new Date()
// 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值
if (now - last >= interval) {
// 若是時間間隔大於咱們設定的時間間隔閾值,則執行回調
last = now;
fn.apply(context, args);
}
}
}
複製代碼
節流函數輸入一個函數並返回一個函數(高階函數)。節流使用閉包,保存上一次觸發回調的時間(last),執行函數(fn),時間閥值(interval),在要執行fn時,當前時間與上一次觸發時間進行比較,若是時間間隔大於interval(now - last >= interval)
,執行函數fn.apply(context, args)
。
防抖是「最後一個說了算」,也用上述例子,當在搜索框中每次按下鍵盤時,都啓動一個「定時器」,若是在指定時間內又按下時,清除以前定時器,再新建一個。定時器的特性就是超過delay的時間,觸發fn。那麼咱們實現的代碼以下:
// fn是咱們須要包裝的事件回調, delay是每次推遲執行的等待時間
function debounce(fn, delay) {
// 定時器
let timer = null
// 將debounce處理結果看成函數返回
return function () {
// 保留調用時的this上下文
let context = this
// 保留調用時傳入的參數
let args = arguments
// 每次事件被觸發時,都去清除以前的舊定時器
if(timer) {
clearTimeout(timer)
}
// 設立新定時器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
複製代碼
防抖函數也是一個高階函數,也使用了閉包,與節流不一樣,此處閉包保存的是setTimeout返回的timer,用於在後續持續觸發以前及時取消定時器。
理解防抖和節流的概念和基本實現(這部分須要講出原理,手寫實現)。
下面來看一下underscore對於節流和防抖的實現
理解了上述節流和防抖的實現,再來看underscore的源碼就會容易不少。下面貼上代碼實現,我在上面加了註釋
_.throttle = function(func, wait, options) {
//timeout存儲定時器 context存儲上下文 args存儲func的參數 result存儲func執行的結果
var timeout, context, args, result;
var previous = 0;//記錄上一次執行func的時間,默認0,也就是第一次func必定執行(now-0)大於wait
if (!options) options = {};//默認options
//定時器函數
var later = function() {
//記錄此次函數執行時間
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);//執行函數func
if (!timeout) context = args = null;
};
var throttled = function() {
var now = _.now();//當前時間
//若是第一次不執行,previous等於當前時間
if (!previous && options.leading === false) previous = now;
//時間間隔-(當前時間-上一次執行時間)
var remaining = wait - (now - previous);
context = this;
args = arguments;
//若是remaining<0,那麼距離上次執行時間超過wait,若是(now-previous)<0,也就是now<previous
if (remaining <= 0 || remaining > wait) {
//清除定時器
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;//記錄當前執行時間
result = func.apply(context, args);//執行函數func
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
//若是不由用最後一次執行(trailing爲true),定時執行func
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
複製代碼
underscore的節流函數多了options
參數,其中options
有兩個配置項leading
和trailing
,由於在節流函數默認的第一時間儘快執行這個func(previous=0)
,若是你想禁用第一次首先執行的話,傳遞{leading:false}
,若是你想禁用最後一次執行的話,傳遞{trailing: false}
。
underscore使用if (!previous && options.leading === false) previous = now
來禁止首次執行,這樣後續的remaining等於1000
,不會進入if的第一個條件體內,因此不會當即執行。
underscore使用定時器來控制最後一次是否須要執行,if (!timeout && options.trailing !==false)
代表若是trailing
設置false
那麼就不會觸發定時器,也就不會執行。默認是能夠執行最後一次,由於option.trailing=undefined,undefined!==false是true
,因此能夠執行定時器。
_.debounce = function(func, wait, immediate) {
//timeout存儲定時器的返回值 result返回func的結果
var timeout, result;
//定時器觸發函數
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);//若是存在定時器,先清除原先的定時器
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);//啓動一個定時器
if (callNow) result = func.apply(this, args);//若是immediate爲true,那麼當即執行函數
} else {
timeout = _.delay(later, wait, this, args);//一樣啓動一個定時器
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
複製代碼
underscore的debouce函數多了immediate
參數,當immediate
爲 true, debounce會在 wait 時間間隔的開始調用這個函數 。
節流和防抖是JavaScript中一個很是重要的知識點,咱們首先要知道節流是「第一個說了算」,後續都會被節流閥屏蔽掉,防抖是「最後一個說了算」,邪惡的魔鬼每一個多會啓動一個定時炸彈,只有後面的定時炸彈到了纔會拆掉前面的炸彈,可是最後仍是會延遲起爆。根據這個思想,咱們利用閉包的思想可以手寫實現它們。根據underscore的源碼咱們可以更好更靈活的利用它們。
最後我在貼一個道友製做的地址能夠幫助咱們直觀的理解節流和防抖。
另外《underscore源碼系列》已經整理至語雀,點擊這裏