這個例子有趣很多。
throttle
throttle 的直翻是「節流」。
如果你跟我一樣無聊,有研究過汽機車的引擎運作原理的話,那你應該知道「節流閥」是什麼,還有它的用途。throttle 的意思跟它還蠻類似的(我覺得啦)。
當然,沒研究過的話也沒關係。
簡單來說,throttle 的概念可以想成是一扇門,當有一個人通過這扇門,這扇門就會關起來,必須等一段時間後這扇門才會再次打開,讓下一個人通過。throttle 就是一直循環這個流程:
有人通過 > 把門關起來 > 等待 n 秒 > 把門打開
所以利用 throttle 就能控制每隔幾秒讓一個人通過,這就是它的用途。
回到前端的部分,這可以用在你不希望「某個事件在短時間內重複觸發」,例如最常見的:scroll
:
先來看沒有 throttle 的情況:
加了 throttle 以後(delay 時間為 250ms):
實作 throttle
這邊的實作方式是寫一個 throttle
函式,接收兩個參數:
fn
要執行的 function
delay
要等待的時間
把 function 丟進去後會回傳一個新的 function,這個 function 必須每隔幾秒才能 call 一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function throttle (fn, delay) { let isTimeout = true; function newFunc (...argu) { if (isTimeout) { isTimeout = false; fn(...argu); setTimeout(() => isTimeout = true, delay) } } return newFunc } module.exports = throttle;
|
這邊被關在閉包中的變數為:isTimeout
、delay
和 fn
最後附個測試檔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const throttle = require('./throttle'); describe("throttle", () => { it('can not retrigger function during the time', async () => { const fn = jest.fn(); const throttleFn = throttle(fn, 250); throttleFn(10); expect(fn).toHaveBeenCalledWith(10); expect(fn.mock.calls.length).toBe(1);
await sleep(250); throttleFn(20) expect(fn).toHaveBeenCalledWith(20); expect(fn.mock.calls.length).toBe(2);
await sleep(250); throttleFn("b", "a"); for (let i=1; i<=100; i++) { throttleFn("a", "b", "c"); } expect(fn).toHaveBeenCalledWith('b', 'a'); expect(fn.mock.calls.length).toBe(3); }) })
|
debounce
debounce 的直翻是「防抖」。
debounce 跟 throttle 的概念有點「類似」(只是觀念類似,但用途差很多),但我暫時想不到比較生活化的例子,所以直接拿前端的實際應用來舉例。
用過 google 搜尋的話應該都知道輸入文字時會有 auto complete 的功能:
這個功能的實作大概會長這樣:
1 2 3 4 5 6 7
| $('input').on('input', async function () { const value = this.value; const result = await getAutoComplete(value); })
|
但這樣有個問題,如果搜尋「花生醬批發」,程式會這樣跑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 取得關鍵字「花」 發 request 查詢跟「花」相關的關鍵字 把結果渲染到畫面上
取得關鍵字「生」 發 request 查詢跟「生」相關的關鍵字 把結果渲染到畫面上
取得關鍵字「醬」 發 request 查詢跟「醬」相關的關鍵字 把結果渲染到畫面上
取得關鍵字「批」 發 request 查詢跟「批」相關的關鍵字 把結果渲染到畫面上
取得關鍵字「醬」 發 request 查詢跟「發」相關的關鍵字 把結果渲染到畫面上
|
問題是指我每打一個字,就會發一個 request 到後端去。
這樣子的效能不好,明明我只是想搜尋「花生醬批發」而已,卻要發 5 次 request。
比較好的做法是:
在輸入文字時,先設定一段時間(這段時間被稱為「threshold 閾值」),在這期間輸入文字的話就得重新等待,必須等到時間結束時都沒有輸入文字,才會發 request 去拿資料。
這個就是 debounce 的作用。
接著來演示一段沒有 debounce 的情況:
加了 debounce 以後:
實作 debounce
這邊一樣會實作一個函式 debounce
,接收兩個參數:
把 function 丟進去後會回傳一個新的 function,這個 function 會以 debounce 的方式來執行:
1 2 3 4 5 6 7 8 9 10 11
| function debounce(fn, delay) { let timer = null; function newFunc (...argu) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => fn(...argu), delay); } return newFunc; } module.exports = debounce;
|
一樣附上測試檔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const debounce = require('./debounce'); describe("debounce", () => { it('should trigger function after certain delay', async () => { const fn = jest.fn(); const debouncedFn = debounce(fn, 250); debouncedFn(10); expect(fn).not.toHaveBeenCalledWith();
await sleep(250); expect(fn).toHaveBeenCalledWith(10); expect(fn.mock.calls.length).toBe(1);
debouncedFn("b", "a"); debouncedFn("d", "e"); debouncedFn("a", "b", "c"); expect(fn.mock.calls.length).toBe(1); await sleep(250); expect(fn.mock.calls.length).toBe(2); expect(fn).toHaveBeenCalledWith("a", "b", "c"); }) })
|