函式去抖(debounce)和函式節流(throttle)
目的
以下場景往往由於事件頻繁被觸發,因而頻繁執行DOM操作、資源載入等重行為,導致UI停頓甚至瀏覽器崩潰。
window
物件的resize
、scroll
事件- 拖拽時的
mousemove
事件 - 射擊遊戲中的
mousedown
、keydown
事件 - 文字輸入、自動完成的
keyup
事件
實際上對於window的resize
事件,實際需求大多為停止改變大小n
毫秒後執行後續處理;而其他事件大多的需求是以一定的頻率執行後續處理。針對這兩種需求就出現了debounce和throttle兩種解決辦法。
throttle(又稱節流)和debounce(又稱去抖)其實都是函式呼叫頻率的控制器,
debounce去抖
當呼叫函式n
秒後,才會執行該動作,若在這n
秒內又呼叫該函式則將取消前一次並重新計算執行時間,舉個簡單的例子,我們要根據使用者輸入做suggest,每當使用者按下鍵盤的時候都可以取消前一次,並且只關心最後一次輸入的時間就行了。
_.debounce(func, [wait=0], [options={}])
lodash在opitons
引數中定義了一些選項,主要是以下三個:
leading
,函式在每個等待時延的開始被呼叫,預設值為false
trailing
,函式在每個等待時延的結束被呼叫,預設值是true
maxwait
,最大的等待時間,因為如果debounce的函式呼叫時間不滿足條件,可能永遠都無法觸發,因此增加了這個配置,保證大於一段時間後一定能執行一次函式
根據leading
和trailing
的組合,可以實現不同的呼叫效果:
leading
-false
,trailing
-true
:預設情況,即在延時結束後才會呼叫函式leading
-true
,trailing
-true
:在延時開始時就呼叫,延時結束後也會呼叫leading
-true
,trailing
-false
:只在延時開始時呼叫
deboucne還有cancel
方法,用於取消防抖動呼叫
下面是一些簡單的用例:
// 避免視窗在變動時出現昂貴的計算開銷。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
// 當點選時 `sendMail` 隨後就被呼叫。
jQuery(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
// 確保 `batchLog` 呼叫1次之後,1秒內會被觸發。
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
// 取消一個 trailing 的防抖動呼叫
jQuery(window).on('popstate', debounced.cancel);
在學習Vue的時候,官網也用到了一個裡子,就是用於對使用者輸入的事件進行了去抖,因為使用者輸入後需要進行ajax
請求,如果不進行去抖會頻繁的傳送ajax
請求,所以通過debounce對ajax
請求的頻率進行了限制
完整的demo在這裡。
methods: {
// `_.debounce` 是一個通過 Lodash 限制操作頻率的函式。
// 在這個例子中,我們希望限制訪問 yesno.wtf/api 的頻率
// AJAX 請求直到使用者輸入完畢才會發出。想要了解更多關於
getAnswer: _.debounce(function() {
if (!reg.test(this.question)) {
this.answer = 'Questions usually end with a question mark. ;-)';
return;
}
this.answer = 'Thinking ... ';
let self = this;
axios.get('https://yesno.wtf/api')
// then中的函式如果不是箭頭函式,則需要對this賦值self
.then((response) = > {
this.answer = _.capitalize(response.data.answer)
}).
catch ((error) = > {
this.answer = 'Error! Could not reach the API. ' + error
})
}, 500) // 這是我們為判定使用者停止輸入等待的毫秒數
},
簡單的實現
一個簡單的手寫的去抖函式:
function test() {
console.log(11)
}
function debounce(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context)
}, 500)
}
window.onresize = function() {
debounce(test, window);
}
lodash中debounce的原始碼學習
function debounce(func, wait, options) {
var nativeMax = Math.max,
toNumber,
nativeMin
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
// func 上一次執行的時間
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
// func必須是函式
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
// 對間隔時間的處理
wait = toNumber(wait) || 0;
// 對options中傳入引數的處理
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
// 執行要被觸發的函式
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
// 在leading edge階段執行函式
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// 為 trailing edge 觸發函式呼叫設定定時器
timerId = setTimeout(timerExpired, wait);
// leading = true 執行函式
return leading ? invokeFunc(time) : result;
}
// 剩餘時間
function remainingWait(time) {
// 距離上次debounced函式被呼叫的時間
var timeSinceLastCall = time - lastCallTime,
// 距離上次函式被執行的時間
timeSinceLastInvoke = time - lastInvokeTime,
// 用 wait 減去 timeSinceLastCall 計算出下一次trailing的位置
result = wait - timeSinceLastCall;
// 兩種情況
// 有maxing: 比較出下一次maxing和下一次trailing的最小值,作為下一次函式要執行的時間
// 無maxing: 在下一次trailing時執行timerExpired
return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
}
// 根據時間判斷 func 能否被執行
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// 幾種滿足條件的情況
return (lastCallTime === undefined // 首次執行
|| (timeSinceLastCall >= wait) // 距離上次被呼叫已經超過 wait
|| (timeSinceLastCall < 0)// 系統時間倒退
|| (maxing && timeSinceLastInvoke >= maxWait)); //超過最大等待時間
}
// 在 trailing edge 且時間符合條件時,呼叫 trailingEdge函式,否則重啟定時器
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 重啟定時器
timerId = setTimeout(timerExpired, remainingWait(time));
}
// 在trailing edge階段執行函式
function trailingEdge(time) {
timerId = undefined;
// 有lastArgs才執行,
// 意味著只有 func 已經被 debounced 過一次以後才會在 trailing edge 執行
if (trailing && lastArgs) {
return invokeFunc(time);
}
// 每次 trailingEdge 都會清除 lastArgs 和 lastThis,目的是避免最後一次函式被執行了兩次
// 舉個例子:最後一次函式執行的時候,可能恰巧是前一次的 trailing edge,函式被呼叫,而這個函式又需要在自己時延的 trailing edge 觸發,導致觸發多次
lastArgs = lastThis = undefined;
return result;
}
// cancel方法
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
// flush方法--立即呼叫
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
//是否滿足時間條件
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time; //函式被呼叫的時間
// 無timerId的情況有兩種:
// 1.首次呼叫
// 2.trailingEdge執行過函式
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
// 負責一種case:trailing 為 true 的情況下,在前一個 wait 的 trailingEdge 已經執行了函式;
// 而這次函式被呼叫時 shouldInvoke 不滿足條件,因此要設定定時器,在本次的 trailingEdge 保證函式被執行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
throttle節流
throttle將一個函式的呼叫頻率限制在一定閾值內,例如1s
內一個函式不能被呼叫兩次。
同樣,lodash提供了這個方法:
_.throttle(func, [wait=0], [options={}])
具體使用的例子:
// 避免在滾動時過分的更新定位
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
// 點選後就呼叫 `renewToken`,但5分鐘內超過1次。
var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
jQuery(element).on('click', throttled);
// 取消一個 trailing 的節流呼叫。
jQuery(window).on('popstate', throttled.cancel);
throttle同樣提供了leading
和trailing
引數,與debounce含義相同
其實throttle就是設定了
maxwait
的debounce
坑
注意,debounce返回的是一個經過包裝的函式,被包裝的函式必須是要立刻執行的函式。例如:
function test() {
console.log(123)
}
setInterval(function () {
_.debounce(test, 1500)
}, 500)
上面的效果不會是我們想要的效果,因為每次setInterval
執行之後,都返回了一個沒有執行的、經過debounce包裝後的函式,所以debounce是無效的
點選事件也是同樣:
btn.addEventListener('click', function () {
_.debounce(test, 1500)
})
上面的程式碼同樣不會生效,正確的做法是:
btn.addEventListener('click', test)
setInterval(_.debounce(test, 1500), 500)