【跟著大佬學JavaScript】之節流

易函123發表於2022-07-07

前言

js的典型的場景

  • 監聽頁面的scroll事件
  • 拖拽事件
  • 監聽滑鼠的 mousemove 事件
    ...

這些事件會頻繁觸發會影響效能,如果使用節流,降低頻次,保留了使用者體驗,又提升了執行速度,節省資源。

原理

節流的原理:持續觸發某事件,每隔一段時間,只執行一次。

通俗點說,3 秒內多次呼叫函式,但是在 3 秒間隔內只執行一次,第一次執行後 3 秒 無視後面所有的函式呼叫請求,也不會延長時間間隔。3 秒間隔結束後則開始執行新的函式呼叫請求,然後在這新的 3 秒內依舊無視後面所有的函式呼叫請求,以此類推。

簡單來說:每隔單位時間( 3 秒),只執行一次。

實現方式

目前比較主流的實現方式有兩種:時間戳、定時器。

時間戳實現

使用時間戳實現:首先初始化執行事件的時間previous為0,然後將當前的時間戳減去上次執行時間(now - previous),如果大於wait,則直接執行函式,並且將此時的執行時間now賦給previous(previous = now)。

由於首次previous = 0,則此時函式第一次觸發就會立即執行。

後續則每隔wait時間執行一次,如果停止觸發,則不會再執行函式。

// 由於一開始now - 0 > wait,則這個寫法,時間會立即執行,沒過一秒會執行一次,停止觸發,則不會再執行事件
function throttle(func, wait = 500) {
    let context, now;
    let previous = 0; // 設定過去的執行時間初始值為0
    return function (...args) {
        context = this;
        now = +(Date.now() || new Date().getTime());
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    };
}

定時器實現

使用定時器實現:首先初始化timeout,然後定義!timeout為true的情況下,直接執行setTimeout,,等待wait時間後執行函式,然後清空timeout,以此類推,重新進入也會按上述執行。

由於進入函式,就執行setTimeout,所以不會立即觸發函式執行。

後續則每隔wait時間執行一次,如果停止觸發,而後還會觸發執行一次函式。

// 由於一進入就建立了定時器,所以不會立即觸發函式執行
function throttle(func, wait = 500) {
    let context, timeout;
    
    return function (...args) {
        context = this;
        
        if (!timeout) {
            timeout = setTimeout(function () {
                timeout = null;
                func.apply(context, args);
            }, wait);
        }
    };
}

合併版本

如果,我們需要既剛開始就立即執行,停止觸發後,還會觸發執行一次函式。

下面,我們將定時器和時間戳合併,組成一個全新的節流版本。

function throttle(func, wait = 500) {
    let context, timeout, result;
    let previous = 0;
    const throttled = function (...args) {
        context = this;
        const now = +(Date.now() || new Date().getTime()); // 當前時間
        // 下次觸發 func 剩餘時間
        const remaining = wait - (now - previous);
        
        // 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            // 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = +(Date.now() || new Date().getTime()); // 這裡是將previous重新賦值當前時間
                func.apply(context, args);
            }, remaining);
        }
    };
    return throttled;
}

合併版本優化

由於合併後的版本並沒用返回值的優化+取消功能。

下面對程式碼進行返回值+取消功能優化:

function throttle(func, wait = 500) {
    let context, timeout, result;
    let previous = 0;
    
    const showResult = function (e1, e2) {
        result = func.apply(e1, e2);
        return result;
    };
    
    const throttled = function (...args) {
        context = this;
        const now = +(Date.now() || new Date().getTime()); // 當前時間
        // 下次觸發 func 剩餘時間
        const remaining = wait - (now - previous);
        
        // 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            return showResult(context, args);
        } else if (!timeout) {
            // 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = +(Date.now() || new Date().getTime()); // 這裡是將previous重新賦值當前時間
                return showResult(context, args);
            }, remaining);
        }
        retrun result
    };
    
    throttled.cancel = function () {
        if (timeout !== undefined) {
            clearTimeout(timeout);
        }
        previous = 0;
        context = timeout = result = undefined;
    };
    return throttled;
}

功能性優化

有時候,我們也希望無頭有尾,或者有頭無尾。

function throttle(func, wait = 500, options = {}) {
    let context, timeout, result;
    let previous = 0;
    
   // 如果同時設定無頭無尾,則直接使用預設設定,其他情況,則走下述操作
    if (!(options.leading === false && options.trailing === false)) {
        leading = !!options.leading; // 預設去除立即執行部分
        trailing = "trailing" in options ? !!options.trailing : true; // 預設保留尾部
    }
    
    // 返回原函式的return
    const showResult = function (e1, e2) {
        result = func.apply(e1, e2);
        return result;
    };
    
    // 獲取當前時間
    const getNow = function () {
        return +(Date.now() || new Date().getTime());
    };
    
    const throttled = function (...args) {
        context = this;
        const now = getNow(); // 當前時間
        // 下次觸發 func 剩餘時間
        if (!previous && leading === false) previous = now;
        const remaining = wait - (now - previous);
        
        // 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            return showResult(context, args);
        } else if (!timeout && trailing !== false) {
            // 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
            timeout = setTimeout(function () {
                timeout = null;
                previous = options.leading === false ? 0 : getNow(); // 這裡是將previous重新賦值當前時間
                return showResult(context, args);
            }, remaining);
        }
        return result;
    };
    
    throttled.cancel = function () {
        if (timeout !== undefined) {
            clearTimeout(timeout);
        }
        previous = 0;
        context = timeout = result = undefined;
    };
    return throttled;
}

這裡,如果options不傳引數,函式預設設定

let leading = false
let trailing = true

也就是無頭有尾。

如果同時設定無頭無尾,則會直接採用預設設定,無頭有尾。

// 如果同時設定無頭無尾,則直接使用預設設定,其他情況,則走下述操作
if (!(options.leading === false && options.trailing === false)) {
    leading = !!options.leading; // 預設去除立即執行部分
    trailing = "trailing" in options ? !!options.trailing : true; // 預設保留尾部
}

演示地址

可以去Github倉庫檢視演示程式碼

跟著大佬學系列

主要是日常對每個進階知識點的摸透,跟著大佬一起去深入瞭解JavaScript的語言藝術。

後續會一直更新,希望各位看官不要吝嗇手中的贊。

❤️ 感謝各位的支援!!!

❤️ 如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!!!

❤️ 喜歡或者有所啟發,歡迎 star!!!

參考

原文地址

【跟著大佬學JavaScript】之節流

相關文章