JS專題之節流函式

南波發表於2018-12-12

上一篇文章講了去抖函式,然後這一篇講同樣為了優化效能,降低事件處理頻率的節流函式。

一、什麼是節流?

節流函式(throttle)就是讓事件處理函式(handler)在大於等於執行週期時才能執行,週期之內不執行,即事件一直被觸發,那麼事件將會按每小段固定時間一次的頻率執行。

打個比方:王者榮耀、英雄聯盟、植物大戰殭屍遊戲中,技能的冷卻時間,技能的冷卻過程中,是無法使用技能的,只能等冷卻時間到之後才能執行。

那什麼樣的場景能用到節流函式呢?
比如:

  1. 頁面滾動和改變大小時需要進行業務處理,比如判斷是否滑到底部,然後進行懶載入資料。
  2. 按鈕被高頻率地點選時,比如遊戲和搶購網站。

我們通過一個簡單的示意來理解:

JS專題之節流函式

節流函式可以用時間戳和定時器兩種方式進行處理。

二、時間戳方式實現

<div class="container" id="container">
    正在滑動:0
</div>

<script>
window.onload = function() {
    var bodyEl = document.getElementsByTagName("body")[0]
}

var count = 0;
window.onmousemove = throttle(eventHandler, 1000);

function eventHandler(e) {
    var containerEl = document.getElementById("container");
    containerEl.innerHTML = "正在滑動: " + count;
    count++;
}

function throttle(func, delay) {
    var delay = delay || 1000;
    var previousDate = new Date();
    var previous = previousDate.getTime();  // 初始化一個時間,也作為高頻率事件判斷事件間隔的變數,通過閉包進行儲存。
    
    return function(args) {
        var context = this;
        var nowDate = new Date();
        var now = nowDate.getTime();
        if (now - previous >= delay) {  // 如果本次觸發和上次觸發的時間間隔超過設定的時間
            func.call(context, args);  // 就執行事件處理函式 (eventHandler)
            previous = now;  // 然後將本次的觸發時間,作為下次觸發事件的參考時間。
        }
    }
}
</script>
複製程式碼

看時間戳實現版本的效果:

JS專題之節流函式

三、定時器方式實現

<div class="container" id="container">
    正在滑動: 0
</div>

<script>
window.onload = function() {
    var bodyEl = document.getElementsByTagName("body")[0]
}

var count = 0;
window.onmousemove = throttle(eventHandler, 1000);

function eventHandler(e) {
    var containerEl = document.getElementById("container");
    containerEl.innerHTML = "正在滑動: " + count;
    count++;
}


function throttle(func, delay) {
    var delay = delay || 1000;
    var timer = null;
    return function(args) {
        var context = this;
        var nowDate = new Date();
        var now = nowDate.getTime();
        if (!timer) {
            timer = setTimeout(function() {
                func.call(context, args);
                timer = null;
            }, delay)

        }
    }

}
</script>
複製程式碼

看看定時器版實現版本的效果:

JS專題之節流函式

三、時間戳和定時器的對比分析

對比時間戳和定時器兩種方式,效果上的區別主要在於:

事件戳方式會立即執行,定時器會在事件觸發後延遲執行,而且事件停止觸發後還會再延遲執行一次。

具體選擇哪種方式取決於使用場景。underscore 把這兩類場景用 leading 和 trailing 進行了表示。

四、underscore 原始碼實現

underscore 的原始碼中就同時實現了時間戳和定時器實現方式,在呼叫時可以自由選擇要不要在間隔時間開始時(leading)執行,或是間隔時間結束後(trailing)執行。

具體看虛擬碼和示意圖:

<div class="container" id="container">
        正在滑動: 0
    </div>
    <div class="height"></div>
    <script>
    window.onload = function() {
        var bodyEl = document.getElementsByTagName("body")[0]
    }

    var count = 0;

    // 事件處理函式
    function eventHandler(e) {
        var containerEl = document.getElementById("container");
        containerEl.innerHTML = "正在滑動: " + count;
        count++;
    }

    var _throttle = function(func, wait, options) {
        var context, args, result;

        // 定時器變數預設為 null, 是為了如果想要觸發了一次後再延遲執行一次。
        var timeout = null;

        // 上一次觸發事件回撥的時間戳。 預設為 0 是為了方便第一次觸發預設立即執行
        var previous = 0;

        // 如果沒有傳入 options 引數
        // 則將 options 引數置為空物件
        if (!options)
            options = {};

        var later = function() {
            // 如果 options.leading === false
            // 則每次觸發回撥後將 previous 置為 0, 表示下次事件觸發會立即執行事件處理函式
            // 否則置為當前時間戳
            previous = options.leading === false ? 0 : +new Date();

            // 剩餘時間跑完,執行事件,並把定時器變數置為空,如果不為空,那麼剩餘時間內是不會執行事件處理函式的,見 else if 那。
            timeout = null;

            result = func.apply(context, args);

            // 剩餘時間結束,並執行完事件後,清理閉包中自由變數的記憶體垃圾,因為不再需要了。
            if (!timeout)
                context = args = null;
        };


        // 返回的事件回撥函式
        return function() {
            // 記錄當前時間戳
            var now = +new Date();

            // 第一次執行回撥(此時 previous 為 0,之後 previous 值為上一次時間戳)
            // 並且如果程式設定第一個回撥不是立即執行的(options.leading === false)
            // 則將 previous 值(表示上次執行的時間戳)設為 now 的時間戳(第一次觸發時)
            // 表示剛執行過,這次就不用執行了
            if (!previous && options.leading === false)
                previous = now;

            // 間隔時間 和 上一次到本次事件觸發回撥的持續時間的時間差
            var remaining = wait - (now - previous);

            context = this;
            args = arguments;

            // 如果間隔時間還沒跑完,則不會執行任何事件處理函式。
            // 如果超過間隔時間,就可以觸發方法(remaining <= 0)

            // remaining > wait,表示客戶端系統時間被調整過
            // 也會立即執行 func 函式

            if (remaining <= 0 || remaining > wait) {
                if (timeout) {
                    clearTimeout(timeout);
                    // 解除引用,防止記憶體洩露
                    timeout = null;
                }

                // 重置前一次觸發的時間戳
                previous = now;

                // result 為事件處理函式(handler)的返回值
                // 採用 apply 傳遞類陣列物件 arguments
                result = func.apply(context, args);

                // 引用置為空,防止記憶體洩露
                if (!timeout)
                    context = args = null;

            } else if (!timeout && options.trailing !== false) {
                // 如果 remaining > 0, 表示在間隔時間內,又觸發了一次事件

                // 如果 trailing 為真,則會在間隔時間結束時執行一次事件處理函式(handler)
                // 在從觸發到剩餘時間跑完,會利用一個定時器執行事件處理函式,並在定時器結束時把 定時器變數置為空

                // 如果剩餘事件內已經存在一個定時器,則不會進入本  else if 分支, 表示剩餘時間已經有一個定時器在執行,該定時器會在剩餘時間跑完後執行。
                // 如果 trailing =  false,即不需要在剩餘時間跑完執行事件處理函式。
                // 間隔 remaining milliseconds 後觸發 later 方法
                timeout = setTimeout(later, remaining);
            }

            // 回撥返回值
            return result;
        };
    };

    window.onmousemove = _throttle(eventHandler, 1000);
    </script>
複製程式碼

下面是我畫的示意圖:

JS專題之節流函式
大致總結一下程式碼對事件處理邏輯的影響:

  1. 如果 leading 為真,那麼綠色意味著間隔時間的開始會立即執行,第一次觸發也會立即執行。
  2. 如果 trailing 為真,那麼從藍紫色的豎細線後的剩餘事件,會跑一個定時器,定時器在時間間隔結束時再執行一次事件處理函式。
  3. 如果 leading 不為真,那麼第一次事件觸發不會立即執行。
  4. 如果 trailing 不為真,最後一次事件觸發後,不然再執行一次事件處理函式。

節流和去抖的常見場景

  1. 輸入框打字輸入完後才開始非同步請求資料校驗內容:去抖
  2. 下拉滾動條判斷是否到底部,進行懶載入資料:去抖和節流都可以,判斷是否到底的方式不同
  3. 活動網站、遊戲網站,按鈕被瘋狂點選:節流

五、總結

去抖和節流函式都是為了降低高頻率事件觸發的事件處理頻率,從而優化網頁中大量重繪重排帶來的效能問題。

其區別在於去抖會在高頻率事件觸發時,只執行一次,節流會在滿足間隔時間後執行一次。去抖的 immediate,節流中的 leading, trailing 都是為了儘可能滿足這類工具函式的不同使用場景。

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JS專題之節流函式

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript之原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply
  13. JavaScript專題之模擬實現bind
  14. JavaScript專題之模擬實現new
  15. JS專題之事件模型
  16. JS專題之事件迴圈
  17. JS專題之去抖函式

相關文章