效能優化之節流函式---throttle

你的聲先生發表於2019-05-08

防抖函式和節流函式本質是不一樣的。防抖函式是將多次執行變為最後一次執行或第一次執行,節流函式是將多次執行變成每隔一段時間執行。

節流函式

前言

防抖函式和節流函式本質是不一樣的。防抖函式是將多次執行變為最後一次執行或第一次執行,節流函式是將多次執行變成每隔一段時間執行。

比如說,噹噹我們做圖片懶載入(lazyload)時,需要通過滾動位置,實時顯示圖片時,如果使用防抖函式,懶載入(lazyload)函式將會不斷被延時, 當我們做圖片懶載入(lazyload)時,需要通過滾動位置,實時顯示圖片時,如果使用防抖函式,懶載入(lazyload)函式將會不斷被延時, 只有停下來的時候才會被執行,對於這種需要週期性觸發事件的情況,防抖函式就顯得不是很友好了,此時就應該使用節流函式來實現了。

例子

<div id="container"></div>
複製程式碼
div{
    height: 200px;
    line-height: 200px;
    text-align: center; color: #fff;
    background-color: #444;
    font-size: 25px;
    border-radius: 3px;
}
複製程式碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
    container.innerHTML = count ++ ;
}
container.addEventListener('mousemove',updateCount);
複製程式碼

我們來看一下效果:

avatar

我們可以看到,滑鼠從左側滑到右側,我們繫結的事件執行了119次

節流函式的實現

現在我們來實現一個節流函式,使得滑鼠移動過程中每間隔一段時間事件觸發一次。

使用時間戳來實現節流

首先我們想到使用時間戳計時的方式,每次事件執行時獲取當前時間並進行比較判斷是否執行事件。

/**
 * 節流函式
 * @param func 使用者傳入的節流函式
 * @param wait 間隔的時間
 */
const throttle = function (func,wait = 50) {
    let preTime = 0;
    return function (...args) {
        let now = Date.now();
        if(now - preTime >= wait){
            func.apply(this,args);
            preTime = now;
        }
    }
};
複製程式碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
    container.innerHTML = count ++ ;
}
let action = throttle(updateCount,1000);

container.addEventListener('mousemove',action);
複製程式碼

avatar

此時當滑鼠移入的時候,事件立即執行,在滑鼠移動的過程中,每隔1000ms事件執行一次,旦在最後滑鼠停止移動後,事件不會被執行
此時會有這樣的兩個問題:

  • 如果我們希望滑鼠剛進入的時候不立即觸發事件,此時該怎麼辦呢?
  • 如果我們希望滑鼠停止移動後,等到間隔時間到來的時候,事件依然執行,此時該怎麼辦呢?

使用定時器實現節流

為滿足上面的需求,我們考慮使用定時器來實現節流函式
當事件觸發的時候,我們設定一個定時器,再觸發的時候,定時器存在就不執行,等到定時器執行並執行函式,清空定時器,然後接著設定定時器

/**
 * 節流函式
 * @param func 使用者傳入的節流函式
 * @param wait 間隔的時間
 */
const throttle = function (func,wait = 50) {
    let timer = null;
    return function (...args) {
        if(!timer){
            timer = setTimeout(()=>{
                func.apply(this,args);
                timer = null;
            },wait);
        }
    }
};
複製程式碼

使用這個定時器節流函式應用在最開始的例子上:

let action = throttle(updateCount,2000);

container.addEventListener('mousemove',action);
複製程式碼

avatar

我們可以看到,當滑鼠移入的時候,時間不會立即執行,等待2000ms後執行了一次,此後2000ms執行一次,當滑鼠移除後,前一次觸發事件的時間2000ms後還會觸發一次事件。

比較時間戳節流與定時器節流

  • 時間戳節流
    • 開始時,事件立即執行
    • 停止觸發後,沒有辦法再執行事件
  • 定時器節流
    • 開始時,會在間隔時間後第一次執行
    • 停止觸發後,依然會再次執行一次事件

對於我們日常的工作需求來說,可能出現的需求是,既需要開始時立即執行,也需要結束時還能再執行一次的節流函式。

綜合時間戳節流和定時器節流

/**
 * 節流函式
 * @param func 使用者傳入的節流函式
 * @param wait 間隔的時間
 */
const throttle = function (func,wait = 50) {
    let preTime = 0,
        timer = null;
    return function (...args) {
        let now = Date.now();
        // 沒有剩餘時間 || 修改了系統時間
        if(now - preTime >= wait || preTime > now){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            preTime = now;
            func.apply(this,args);
        }else if(!timer){
            timer = setTimeout(()=>{
                preTime = Date.now();
                timer = null;
                func.apply(this,args)
            },wait - now + preTime);
        }
    }
};
複製程式碼

使用這個定時器節流函式應用在最開始的例子上:

let action = throttle(updateCount,2000);

container.addEventListener('mousemove',action);
複製程式碼

avatar

我們可以看到,當滑鼠移入時,事件立即執行,之後每間隔2000ms後,事件均會執行,當滑鼠離開時,前一次事件觸發2000ms後,事件最後會再一次執行

我們繼續考慮下面的場景

  • 有時候我們的需求變成滑鼠移入時立即執行,滑鼠移除後事件不在執行呢?
  • 有時候我們的需求變成滑鼠移入時不立即執行,滑鼠移除後事件還會執行呢?

繼續優化

我們設定 opts 作為 throttle 函式的第三個引數,然後根據 opts 所攜帶的值來判斷實現那種效果,約定如下:

  • leading : Boolean 是否使用第一次執行
  • trailing : Boolean 是否使用停止觸發的回撥執行

修改程式碼如下:

/**
 * 節流函式
 * @param func 使用者傳入的節流函式
 * @param wait 間隔的時間
 * @param opts leading 是否第一次執行 trailing 是否停止觸發後執行
 */
const throttle = function (func,wait = 50,opts = {}) {
    let preTime = 0,
        timer = null,
        { leading = true, trailing = true } = opts;
    return function (...args) {
        let now = Date.now();
        if(!leading && !preTime){
            preTime = now;
        }
        // 沒有剩餘時間 || 修改了系統時間
        if(now - preTime >= wait || preTime > now){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            preTime = now;
            func.apply(this,args);
        }else if(!timer && trailing){
            timer = setTimeout(()=>{
                preTime = Date.now();
                timer = null;
                func.apply(this,args)
            },wait - now + preTime);
        }
    }
};
複製程式碼

這裡需要注意的是,leading:false 和 trailing: false 不能同時設定。 因為如果同時設定的時候,當滑鼠移除的時候,停止觸發的時候不會設定定時器,也就是說,等到過了設定的時間,preTime不會被更新,此後再次移入的話就會立即執行,就違反了 leading: false

取消

在 debounce 的實現中,我們加了一個 cancel 方法,throttle 我們也加個 cancel 方法:

/**
 * 節流函式
 * @param func 使用者傳入的節流函式
 * @param wait 間隔的時間
 * @param opts leading 是否第一次執行 trailing 是否停止觸發後執行
 */
const throttle = function (func,wait = 50,opts = {}) {
    let preTime = 0,
        timer = null,
        { leading = false, trailing = true } = opts,
        throttled = function (...args) {
            let now = Date.now();
            if(!leading && !preTime){
                preTime = now;
            }
            // 沒有剩餘時間 || 修改了系統時間
            if(now - preTime >= wait || preTime > now){
                if(timer){
                    clearTimeout(timer);
                    timer = null;
                }
                preTime = now;
                func.apply(this,args);
            }else if(!timer && trailing){
                timer = setTimeout(()=>{
                    preTime = Date.now();
                    timer = null;
                    func.apply(this,args)
                },wait - now + preTime);
            }
        };
    throttled.cancel = function () {
        clearTimeout(timer);
        timer = null;
        preTime = 0;
    };
    return throttled;
};
複製程式碼

至此我們完成了一個節流函式。

來自我豐哥,前端大神級開發

相關文章