JS函式節流和函式防抖

苦瓜炒蛋發表於2018-11-30

1.為什麼需要函式防抖和函式節流?

  • 在瀏覽器中某些計算和處理要比其他的昂貴很多。例如DOM操作比起非DOM互動需要更多的記憶體和CPU佔用時間。連續嘗試進行過多的DOM操作可能會導致瀏覽器掛起,甚至崩潰;
  • 例如當調整瀏覽器大小的時候,resize事件會連續觸發;如果在resize事件處理程式內部嘗試進行DOM操作,其高頻率的更改可能會讓瀏覽器崩潰;
  • 為了繞開上面的問題,需要對該類函式進行節流;

2.什麼是函式防抖和函式節流

防抖(debounce)和節流(throttle)都是用來控制某個函式在一定時間內執行多少次的技巧,兩者相似而又不同。 背後的基本思想是某些程式碼不可以在沒有間斷的情況下連續重複執行。

2.1 函式防抖 (debounce)

如果一個事件被頻繁觸發多次,並且觸發的時間間隔過短,則防抖函式可以使得對應的事件處理函式只執行最後觸發的一次。 函式防抖可以把多個順序的呼叫合併成一次。

2.2 函式節流 (throttle)

如果一個事件被頻繁觸發多次,節流函式可以按照固定頻率去執行對應的事件處理方法。 函式節流保證一個事件一定時間內只執行一次。

3.應用場景

型別 場景
函式防抖 1. 手機號、郵箱輸入檢測
2. 搜尋框搜尋輸入(只需最後一次輸入完後,再放鬆Ajax請求)
3. 視窗大小resize(只需視窗調整完成後,計算視窗大小,防止重複渲染)
4.滾動事件scroll(只需執行觸發的最後一次滾動事件的處理程式)
5. 文字輸入的驗證(連續輸入文字後傳送 AJAX 請求進行驗證,(停止輸入後)驗證一次就好
函式節流 1. DOM元素的拖拽功能實現(mousemove
2. 射擊遊戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
3. 計算滑鼠移動的距離(mousemove
4. 搜尋聯想(keyup
5. 滾動事件scroll,(只要頁面滾動就會間隔一段時間判斷一次)

4.如何實現

4.1 函式防抖實現

function debounce(fn, delay, scope) {
    let timer = null;
    // 返回函式對debounce作用域形成閉包
    return function () {
        // setTimeout()中用到函式環境總是window,故需要當前環境的副本;
        let context = scope || this, args = arguments;
        // 如果事件被觸發,清除timer並重新開始計時
        clearTimeout(timer);
        timer = setTimeout(function () {
            fn.apply(context, args);
        }, delay);
    }
}
複製程式碼
  • 程式碼解讀
  1. 第一次呼叫函式,建立一個定時器,在指定的時間間隔之後執行程式碼;
  2. 當第二次呼叫該函式時,它會清除前一次的定時器並設定另一個;
  3. 如果前一個定時器已經執行過了,這個操作就沒有任何意義;
  4. 然而,如果前一個定時器尚未執行,其實就是將其替換為一個新的定時器;
  5. 目的是只有在執行函式的請求停止了delay時間之後才執行

4.2 函式節流實現

4.2.1 利用時間戳簡單實現

function throttle(fn, threshold, scope) {
    let timer;
    let prev = Date.now();
    return function () {
        let context = scope || this, args = arguments;
        let now = Date.now();
        if (now - prev > threshold) {
            prev = now;
            fn.apply(context, args);
        }
    }
}
複製程式碼

4.2.2 利用定時器簡單實現

function throttle2(fn, threshold, scope) {
    let timer;
    return function () {
        let context = scope || this, args = arguments;
        if (!timer) {
            timer = setTimeout(function () {
                fn.apply(context, args);
                timer = null;
            }, threshold)
        }
    }
}
複製程式碼

5 舉例(scroll事件)

CSS程式碼

    .wrap {
       width: 200px;
        height: 330px;
        margin: 50px;
        margin-top: 200px;
        position: relative;
        float: left;
        background-color: yellow;
    }
    .header{
        width: 100%;
        height: 30px;
        background-color: #a8d4f4;
        text-align: center;
        line-height: 30px;
    }
    .container {
        background-color: pink;
        box-sizing: content-box;
        width: 200px;
        height: 300px;
        overflow: scroll;
        position: relative;
    }
    .content {
        width: 140px;
        height: 800px;
        margin: auto;
        background-color: #14ffb2;
    }
複製程式碼

HTML程式碼

    <div class="wrap">
        <div class="header">滾動事件:普通</div>
        <div class="container">
            <div class="content"></div>
        </div>
    </div>
    <div class="wrap">
        <div class="header">滾動事件:<strong>加了函式防抖</strong></div>
        <div class="container">
            <div class="content"></div>
        </div>
    </div>
    <div class="wrap">
        <div class="header">滾動事件:<strong>加了函式節流</strong></div>
        <div class="container">
            <div class="content"></div>
        </div>
    </div>
複製程式碼

JS程式碼

    let els = document.getElementsByClassName('container');
    let count1 = 0,count2 = 0,count3 = 0;
    const THRESHOLD = 200;

    els[0].addEventListener('scroll', function handle() {
        console.log('普通滾動事件!count1=', ++count1);
    });
    els[1].addEventListener('scroll', debounce(function handle() {
        console.log('執行滾動事件!(函式防抖) count2=', ++count2);
    }, THRESHOLD));
    els[2].addEventListener('scroll', throttle(function handle() {
        console.log(Date.now(),', 執行滾動事件!(函式節流) count3=', ++count3);
    }, THRESHOLD));
複製程式碼
// 函式防抖
function debounce(fn, delay, scope) {
    let timer = null;
    let count = 1;
    return function () {
        let context = scope || this,
            args = arguments;
        clearTimeout(timer);
        console.log(Date.now(), ", 觸發第", count++, "次滾動事件!");
        timer = setTimeout(function () {
            fn.apply(context, args);
            console.log(Date.now(), ", 可見只有當高頻事件停止,最後一次事件觸發的超時呼叫才能在delay時間後執行!");
        }, delay);
    }
}
複製程式碼
// 函式節流
function throttle(fn, threshold, scope) {
    let timer;
    let prev = Date.now();
    return function () {
        let context = scope || this, args = arguments;
        let now = Date.now();
        if (now - prev > threshold) {
            prev = now;
            fn.apply(context, args);
        }
    }
}
複製程式碼

執行結果展示及比較

測試函式防抖和函式節流

普通滾動

防抖滾動

節

6.總結

  • debouncethrottle均是通過減少高頻觸發事件的實際事件處理程式的執行來提高事件處理函式執行效能的手段,並沒有實質上減少事件的觸發次數。
  • debounce可以把多個順序的呼叫合併成一次
  • throttle保證一個事件一定時間內只執行一次。

相關文章