一次搞懂 JavaScript 的 Debounce (去顫)和 Throttle (節流) 詳細教學(附完整程式碼)

Debounce (去顫)和 Throttle (節流) 都是用在網頁中用來防止重複點擊造成重複觸發的。
假設延遲為 1 秒:
Debounce 是點擊後等待 1 秒才觸發,如果 0.5 秒時再被點擊,則重置延遲時間,1.5 秒時才會觸發。
Throttle 則是點擊後馬上觸發,在 1 秒的延遲內不論再被點擊幾次都不會再被觸發。

一般按鈕點擊的基本程式碼:
    
<button id="button">Click</button>

<script>
    const button = document.getElementById('button');

    function clickHandler() {
        console.log('clickHandler');
    }

    button.addEventListener('click', clickHandler);
</script>
    

Debounce (去顫)程式碼說明

首先第一步先建立 debounce 函式,在裡面使用匿名函式將 fun 包裹再傳出。
為什麼要在 debounce 中再回傳一個匿名 function? 如果不這樣做,在按鈕綁定 debounce(clickHandler)) 事件時就會直接觸發一次。
    
function clickHandler() {
    console.log('clickHandler');
}

function debounce(fun) {
    return () => {
        fun();
    };
}

button.addEventListener('click', debounce(clickHandler));
    

使用 setTimeout 加入延遲效果:
    
function debounce(fun,delay) {
    return () => {
        setTimeout(() => {
            fun();
        }, delay);
    };
}

button.addEventListener('click', debounce(clickHandler, 1000));
    

上面是點 10 次後每次點擊雖然都是等到延遲結束後才會觸發,但是都會觸發。現在要將程式碼改為延遲結束前如果再次觸發就重置觸發時間:
    
function debounce(fun, delay) {
    let timer;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fun();
        }, delay);
    };
}

button.addEventListener('click', debounce(clickHandler, 500));
    

現在基本功能就做完了,每次點擊會等到延遲結束後才會觸發,且延遲結束前如果有新的點擊事件會重置延遲時間,在此前間不會被處發。

但是筆者在使用 Vue 時發現一個小問題,以前將 clickHandler 直接綁定到 button 時 this 是指向 button ,而在使用了我們自訂的 debounce 後 clickHandler 內的 this 卻指向了 window 。

要解決這個問題其實也很簡單,匿名函式就不能使用箭頭函式了,然後先將 this 儲存後在匿名函數內呼叫事件時使用 .call() 改變函數內部的 this 就可以了。
    
function debounce(fun, delay) {
    let timer;
    return function () {
        const context = this;
        clearTimeout(timer);
        timer = setTimeout(() => {
            fun.call(context);
        }, delay);
    };
}

button.addEventListener('click', debounce(clickHandler, 500));
    

debounce 完整程式碼

    
<button id="button">Click</button>

<script>
    const button = document.getElementById('button');

    function clickHandler(arg) {
        console.log(this);
        console.log('clickHandler', arg);
    }

    function debounce(fun, delay) {
        let timer;
        return function () {
            const context = this;
            clearTimeout(timer);
            timer = setTimeout(() => {
                fun.call(context);
            }, delay);
        };
    }

    button.addEventListener('click', debounce(()=>clickHandler(12443), 500));
</script>

    

Throttle (節流)程式碼說明

剛剛一步一步示範完 Debounce (去顫) 之後,要了解 Throttle (節流) 就非常簡單了,因為只是先後順序的問題。
Throttle 我們只要改為執行時開啟計時器,只要發現計時器存在就不執行,然後執行時將計時器清空即可
    
<button id="button">Click</button>

<script>
    const button = document.getElementById('button');

    function clickHandler(arg) {
        console.log(this);
        console.log('clickHandler', arg);
    }

    function throttle(fun, delay) {
        let timer;
        return function () {
            const context = this;
            if (timer) return;
            timer = setTimeout(() => {
                fun.call(context);
                timer = null;
            }, delay);
        };
    }

    button.addEventListener('click', throttle(() => clickHandler(12443), 500));
</script>
    

留言