Throttle and Debounce. 使用場景與嘗試實現它們

Throttle and Debounce. 使用場景與嘗試實現它們

February 17, 2023

·

10 min read

假設一個 autocomplete 的需求, input 輸入框,需要在輸入字串的時候,更新下拉選單選項,類似 google search input 的建議,另外狀況是 auto search 的功能,當使用者邊在 input 輸入字串的時候,就一邊會自動送出查詢的功能,這兩者功能如果都沒特別處理,不意外的話,很快就會出意外了。

發生什麼問題

我們就來解釋一下可能發生的問題,這兩者需求都有相同狀態是,短時間內對同一隻 api 快速發出請求,我們就以 auto search 為例子,假設有使用者要嘗試搜尋 banana,那在前面邊打字的時候就會送出 api 請求,假設這之間剛好觸發送出了三次,但是如果 api /query?name=ba 的 response 最晚回來,

至於 api 為什麼會這樣,也可能 database 模糊比對的結果比較多,回傳比較慢之類的,啊就不深究問題了。

這就會導致搜尋結果可能會有 barbapapa 之類的怪東西,就被使用者抱怨,啊這東西搜尋怎跟我想要的不一樣。

無辜的 barbapapa
無辜的 barbapapa

  • 問題發生流程
// input 輸入 `ba`
fetch /query?name=ba

// input 輸入 `bana`
fetch /query?name=bana

// input 輸入 `banana`
fetch /query?name=banana

解決 api response 相互覆蓋問題

講了問題當然就要來討論怎麼解決這些問題,有幾種做法,可以處理掉。

第一種可以取消上一個 api request,fetch、axois 或是 xhrHttpRequest 都可以收回請求,同一隻 api 每次有新的請求,都嘗試去 cancel last request。

第二種就是利用 debounce,讓 api 請求不要頻率這麼的高,debounce 可以設定一定時間內,當有新的 update 進來後,都延長 query 的計時,聽起來有點不知道說啥。

下方是模擬剛剛 input 輸入 banana 搭配 debounce。

  • debounce
// setting debounce callback 1 sec

| o------- 0.6 sec
// 輸入 ba
// 計時一秒後送出查詢
                   o ------- 0.5 sec
                   // 0.6 秒後持續輸入 na
                   // 再度延長計時一秒
                                    o ----------------- |
                                    // 0.5 秒後持續輸入 na
                                    // ( 完成輸入 banana)
                                    // 再度延長計時一秒
                                                        //  一秒後送出查詢!

至於用哪種方法是沒有絕對對錯,也可以都處理,兩者都有各自適合的場景,api cancel 可能會因為使用者打到一半,api 又剛好回傳很快,然後讓頁面有不必要的更新,debounce 就需要時間等待 callback,如果需求不予許等待時間,那就前者做法好一點,那這邊就先不要浪費太多時間去深究各自問題了。

然後如果有其他方法歡迎留言跟我說,感恩。

實現 debounce

秉持著 javascript 沒有黑魔法, 某些特性真的可能有,接下來就來嘗試寫一個 dobounce 吧。

debounce 的功能

一個 debounce 會有兩個參數,第一個會是需要執行的 function,並且可以讓我們帶入參數,第二個就是多久的時間,要執行 callback,然後會再回傳一個 function,讓我們後面可以直接調用。

  • 使用範例
const debounceSearch = debounce((queryStr)=>{
    fetch(`https://jsonplaceholder.typicode.com/todos/1?${queryStr}`)
}, 1000);

function handleSearch(queryStr){
    debounceSearch(queryStr);
}

開始實現 debounce

首先思考的是,這個時間,我們要怎麼透過每一次 callback 後,都延長計時,很直覺想到就是用 setTimeout,這也是 javascript 僅能用的 timer,大概思路就是 set 一個 timer,如果今天有再度被調用的話,就 clear timer,重新再來計時一次,於是,我們就這樣就解決了最魔法的部分了,

  • handle time
function debounce(x, time){
    const timer = setTimeout(function(){}, time);

    clearTimeout(timer);
}

接下來就是處理 callback function 的部分,這邊比較麻煩應該就是要怎麼傳遞參數進去,就直接利用 return function,再透過這邊帶入參數。

function debounce(callback, time){
    return function(...args) {
        setTimeout(function(){
            callback(...args);
        }, time);
    }

我們就快大功告成了,算是完成核心兩個 input callback 以及 time 的部分,剩下就要處理 clearTimer 的時機點。

首先一定要在執行 callback 的時候順便 clearTimer,所以勢必得塞進去 return function 執行緒裡面,那比較困擾的是,要怎樣才能清除上一個 timer,答案就是依賴 closure。

我們先建立一個變數 memoryTimer,寫在外層讓他的記憶體位置一直被保留著,並在執行過一次 callback 後,就讓它 reassign 成為 timer, let memoryTimer; 代表會是 undefined falsely condition,那執行過後再判斷是否為 true,如果為 true 就把 timer 清除掉,就可以收回上一個 setTimeout 了。

  • 完成 debounce
function debounce(callback, time){
    // closure will let memoryTimer memory position
    let memoryTimer;
    return function(...args) {
        if(memoryTimer){
            clearTimeout(timer);
        }
        memoryTimer = setTimeout(function(){
            callback(...args);
        }, time);
    }
}

實現 throttle

都已經完成 debounce 了,就來順手寫一下它的好兄弟 throttle 吧 XDD。throttle 被稱之為節流閥,在指定的時間內,只能被一次性的 callback,像是保護 callback 在一定時間內只允許執行一次。

throttle 的功能

首先在指定的時間內,只能被一次性的 callback,這個 callback 還是由第一個觸發的所執行的。也跟 debounce 一樣有兩個 input,callback function 以及 time,還有需要回傳 function 讓後續使用。

開始實現 throttle

一樣先想一下 throttle 的功能,聽起來跟 debounce 87% 像,就只要注意要怎樣在時間內卡住 callback 不要再被執行。

就反向思考 debounce 來看,debounce 是不斷延長 timer,throttle 反而是依賴 timer,卡住新的 callback,於是我們就一樣看 timer 有沒有存在,存在就完全不做事情,於是就把 condition 換成 if(!memoryTimer){ 來執行 setTimeout。

這邊我是直接 copy paste debounce code,就不贅述 input 那些細節了,邏輯都是一樣的。

function throttle(callback, time){
    // closure will let memoryTimer memory position
    let memoryTimer;
    return function(...args) {
        if(!memoryTimer){
            memoryTimer = setTimeout(function(){
                callback(...args);
            }, time);
        }
    }
}

但這邊就會發現另一個問題了,memoryTimer 在 setTimeout 執行完之後,還是依舊會是 timer,就會導致直接卡死沒辦法繼續執行,那就想辦法讓memoryTimer 可以再度變成 falsely 的狀態,就可以再度重複使用了。

ps. 這邊我原本是寫 clearTimeout(memoryTimer); ,以為可以就這樣讓 memoryTimer 變成 falsely,結果是無法,他依舊會是一個 timer。

function throttle(callback, time){
    // closure will let memoryTimer memory position
    let memoryTimer;
    return function(...args) {
        if(!memoryTimer){
            memoryTimer = setTimeout(function(){
                callback(...args);
                memoryTimer = undefined;
            }, time);
        }
    }
}

心得

以下心得都跟 debounce throttle 無關,純屬個人 murmur。

這篇文章其實是很久很久以前寫到一半的,今天才一口氣補完的。因緣際會在某個面試場合,被面試官稱讚 blog 內容,鼓勵我可以繼續寫文章,說實在當下滿感人的,對方還提出滿多篇內容跟我討論研究,還給我一些更多的解法,真痛哭流涕。

工作了幾年之後,反而會浮出一種心態是,這東西不是很簡單嗎?為什麼要特別寫一篇文章。最近反而開始想不到主題來寫文章了,然後長時間沒有寫作反而讓表達能力下降不少,我自己又有一個老毛病,就是講話很喜歡省字跳來跳過,還是希望透過寫作改善這個問題。

雖然剛剛說東西簡單嗎,但剛剛實現 throttle 還寫錯卡了一下 XDD。

最後就期待自己之後可以用更口語簡單的方式,介紹一些實用的技術。