系列文章:
上一篇文章中介紹的屬性描述符的知識太偏於理論,今天閱讀的 throttle-debounce 模組會實用許多,在工作常常可以用到。
一句話介紹
今天閱讀的 npm 模組是 throttle-debounce,它提供了 throttle
和 debounce
兩個函式:throttle 的含義是節流,debounce 的含義是防抖動,通過它們可以限制函式的執行頻率,避免短時間內函式多次執行造成效能問題,當前包版本為 2.0.1,周下載量為 6.3 萬。
用法
首選需要介紹一下 throttle
和 debounce
,它們都可以用於 函式節流 從而提升效能,但它們還是存在一些不同:
- debounce:將短時間內多次觸發的事件合併成一次事件響應函式執行(往往是在第一次事件或者在最後一次事件觸發時執行),即該段時間內僅一次真正執行事件響應函式。
- throttle:假如在短時間內同一事件多次觸發,那麼每隔一段更小的時間間隔就會執行事件響應函式,即該段時間內可能多次執行事件響應函式。
雖然每天最煩等電梯要花上十幾分鍾,但還是可以用坐電梯來舉例子:
- debounce:假如我在電梯裡面正準備關門,這時 A 想要坐電梯,那麼出於禮貌我會按下開門鍵,然後等他走進電梯後再嘗試關門;等 A 進電梯後,又發現 B 也想要坐電梯,那麼同樣出於禮貌我會按下開門鍵,然後等他走進電梯。那麼假如一直有人想要坐電梯的話,我就會不斷地延後按下關門鍵的時機,直至沒有人想要坐電梯(現實生活中我這樣做的話,估計每天除了坐電梯就可以什麼都不做了)。
- throttle:實際上我每天都有工作要完成,不可能在電梯裡無限地等別人。那麼這回我任性一點,規定我只等 30 秒,不管到時候有沒有人想要坐電梯,我都會按下關門鍵走掉。
從上面兩個例子中可以看出兩者最大的區別在於只要有事件發生(有人想坐電梯),若使用了 throttle
方法,那麼在一段時間內事件響應函式一定會執行(30秒內我按下關門鍵);若使用了 debounce
方法,那麼只有事件停止發生後(我發現沒有人想坐電梯)才會執行。
大家可以嘗試在下面的 Demo 中滾動滑鼠直觀地感受到這兩者的不同:
See the Pen The Difference Between Throttling, Debouncing, and Neither by Elvin Peng (@elvinn) on CodePen.
對於 throttle-debounce,它的簡單用法如下:
import { throttle, debounce } from `throttle-debounce`;
function foo() { console.log(`foo..`); }
function bar() { console.log(`bar..`); }
const fooWrapper = throttle(200, foo);
for (let i = 1; i < 10; i++) {
setTimeout(fooWrapper, i * 30);
}
// => foo 執行了三次
// => foo..
// => foo..
// => foo..
const barWrapper = debounce(200, bar);
for (let i = 1; i < 10; i++) {
setTimeout(barWrapper, i * 30);
}
// => bar 執行了一次
// => bar..
複製程式碼
原始碼學習
throttle 實現
將原始碼簡化後適當修改如下:
// 原始碼 4-1
function throttle(delay, callback) {
let timeoutID;
let lastExec = 0;
function wrapper() {
const self = this;
const elapsed = Number(new Date()) - lastExec;
const args = arguments;
function exec() {
lastExec = Number(new Date());
callback.apply(self, args);
}
clearTimeout(timeoutID);
if (elapsed > delay) {
exec();
} else {
timeoutID = setTimeout(exec, delay - elapsed);
}
}
return wrapper;
}
複製程式碼
整個程式碼的邏輯十分清晰,一共只有三步:
- 計算距離最近一次函式執行後經過的時間
elapsed
,並清除之前設定的計時器。 - 如果經過的時間大於設定的時間間隔
delay
,那麼立即執行函式,並更新最近一次函式的執行時間。 - 如果經過的時間小於設定的時間間隔
delay
,那麼通過setTimeout
設定一個計數器,讓函式在delay - elapsed
時間後執行。
原始碼 4-1 並不難理解,不過需要關注一下 this
的使用:
function throttle(delay, callback) {
// ...
function wrapper() {
const self = this;
const args = arguments;
// ...
function exec() {
// ...
callback.apply(self, args);
}
}
}
複製程式碼
在上面的程式碼中,通過 self
變數臨時儲存 this
的值,從而在 exec
函式中通過 callback.apply(self, args)
傳入正確的 this
值,這種做法在閉包相關的函式呼叫中十分常用。正因為這裡對 this
的處理,所以可以實現下面的能力:
function foo() { console.log(this.name); }
const fooWithName = throttle(200, foo);
const obj = {name: `elvin`};
fooWithName.call(obj, `elvin`);
// => `elvin`
複製程式碼
debounce 實現
由於 debounce
n 只是往後推延函式的執行時間,並不具有 throttle
每隔一段時間一定會執行的能力,所以其實現起來更加簡單:
function debounce(delay, callback) {
let timeoutID;
function wrapper() {
const self = this;
const args = arguments;
function exec() {
callback.apply(self, args);
}
clearTimeout(timeoutID);
timeoutID = setTimeout(exec, delay);
}
return wrapper;
}
複製程式碼
將上述程式碼與 throttle
實現的程式碼相比,可以發現其就是去除了 elapsed
相關邏輯後的程式碼,其餘大部分程式碼一模一樣,所以 debounce
函式可以藉助 throttle
函式實現(throttle-debounce 原始碼中也是這樣做的),throttle
函式也可以藉助 debounce
函式實現。
throttle 和 debounce 使用場景舉例
throttle
和 debounce
適用於使用者短時間內頻繁執行某一相同操作的場景,例如:
- 使用者拖動瀏覽器視窗改變視窗大小,觸發
resize
事件。 - 使用者移動滑鼠,觸發
mousemove
等事件。 - 使用者在輸入框內進入輸入,觸發
keydown
|keypress
|keyinput
|keyup
等事件。 - 使用者滾動螢幕,觸發
scroll
事件。 - 使用者在點選按鈕後,由於 API 請求耗時未立即看到響應,可能會不斷點選按鈕觸發
click
事件。
在網上搜尋了不少資料,發現對兩個函式的使用場景有時彼此之間都互相矛盾,例如有的說在搜尋框進行輸入,應該使用 debounce
進行限流,從而減輕伺服器壓力;有的說使用 throttle
進行限流即可,可以更快地返回使用者的搜尋結果。
在我看來,並不存在一個場景,就一定是使用 throttle
和 debounce
中的一種方法並另外一種方法好,往往需要結合自身的情況進行考慮和選擇:
- 當事件響應函式對 CPU、GPU、流量、伺服器等資源的佔用在接受範圍內時,可以使用
throttle
進行限流帶來更好的使用者體驗。 - 當事件響應函式對 CPU、GPU、流量、伺服器等資源的佔用較大時,可以使用
debounce
進行更強力的限流,從而減輕壓力。
寫在最後
throttle-debounce 原始碼和我前幾天所看的 Sindre 所寫的模組程式碼風格完全不同,它的程式碼中註釋的行數約為程式碼行數的三倍,而且函式的引數均有詳細的註釋,這本應是一件好事,但是對於我閱讀原始碼而言,並沒有覺得更加輕鬆,而求由於對可選引數進行的如下處理,讓我閱讀起來更加費力:
// 原始碼 4-2
/**
*
* @param {Number} delay
* @param {Boolean} [noTrailing]
* @param {Function} callback
* @param {Boolean} [debounceMode]
*
* @return {Function} A new, throttled, function.
*/
export default function ( delay, noTrailing, callback, debounceMode ) {
// `noTrailing` defaults to falsy.
if ( typeof noTrailing !== `boolean` ) {
debounceMode = callback;
callback = noTrailing;
noTrailing = undefined;
}
// ...
}
複製程式碼
在原始碼 4-2 中,從註釋可以看出 noTrailing
和 debounceMode
是可選引數,delay
和 callback 為必選引數,然後它將可選引數 noTrailing
放在了必選引數 callback
之前,再在函式中的程式碼進行判斷:假如 noTrailing
為函式的話,則此值應作為 callback
,然後再將 noTrailing
設為預設值 undefined
。
不禁感嘆這真是一番騷操作,哪怕是為了相容 ES5,也有更好的寫法,這裡說說我個人認為可用 ES6 語法時更好的寫法:
export default function (dalay, noTrailing, options = {
callback = false,
debounceMode = false,
} = {}) {
// ...
}
複製程式碼
關於我:畢業於華科,工作在騰訊,elvin 的部落格 歡迎來訪 ^_^