前端效能優化之防抖-debounce

Codeeeee發表於2018-11-10

這周接到一個需求-給輸入框做模糊匹配。這還不簡單,監聽input事件,取到輸入值去調介面不就行了? 然而後端小哥說不行,這個介面的資料量非常大,這種方式呼叫介面的頻率太高,而且使用者輸入時呼叫根本沒有必要,只要在使用者停止輸入的那一刻切調介面就行了。 唉?這個場景聽起來怎麼這麼像防抖呢?

那到底什麼是防抖呢? 大家一定見過那種左右兩邊中間放廣告位的網站,在網頁滾動時,廣告位要保持在螢幕中間,就要不斷地去計算位置,如果不做限制,在視覺上廣告位就像在“抖”。防止這種情況,就叫防抖了!

防抖的原理是什麼? 我一直覺得網上流傳的例子非常形象:當我們在乘電梯時,如果這時有人過來,我們會出於禮貌一直按著開門按鈕等待,等到這人進電梯了,剛準備關門時,發現又有人過來了!我們又要重複之前的操作,如果電梯空間無限大的話,我們就要一直等待了。。。當然人的耐心是有限的!所以我們規定了一個時間,比如10秒,如果10秒都沒人來的話,就關電梯門。

用專業術語概括就是:在一定時間間隔內函式被觸發多次,但只執行最後一次。

最簡易版的程式碼實現:

function debounce(fn, delay) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        if (timer) {
            clearTimeout(timer);
            timer = null;
        }

        timer = setTimeout(() => {
            fn.apply(context, args);
        }, delay);
    };
}
複製程式碼

fn是要進行防抖的函式,delay是設定的延時,debounce返回一個匿名函式,形成閉包,內部維護了一個私有變數timer。我們一直會觸發的是這個返回的匿名函式,定時器會返回一個Id值賦給timer,如果在delay時間間隔內,匿名函式再次被觸發,定時器都會被清除,然後重新開始計時。

當然簡易版肯定不能滿足日常的需求,比如可能需要第一次立即執行的,所以要稍做改動:

function debounce(fn, delay, immediate) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        timer && clearTimeout(timer);

        if(immediate) {
            const doNow = !timer;

            timer = setTimeout(() => {
                timer = null;
            }, delay);

            doNow && fn.apply(context, args);
        }
        else {
            timer = setTimeout(() => {
                fn.apply(context, args);
            }, delay);
        }
    };
}
複製程式碼

比起簡易版,多了個引數immediate來區分是否需要立即執行。其它與簡易版幾乎一致的邏輯,除了判斷立即執行的地方:

const doNow = !timer;

timer = setTimeout(() => {
    timer = null;
}, delay);

doNow && fn.apply(context, args);
複製程式碼

doNow變數的值為!timer,只有!timer為true的情況下,才會執行fn函式。第一次執行時,timer的初始值為null,所以會立即執行fn。接下來非第一次執行的情況下,等待delay時間後才能再次觸發執行fn。 注意!與簡易版的區別,簡易版是一定時間多次內觸發,執行最後一次。而立即執行版是不會執行最後一次的,需要再次觸發。

防抖的函式可能是有返回值,我們也要做相容:

function debounce(fn, delay, immediate) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;
        let result = undefined;

        timer && clearTimeout(timer);

        if (immediate) {
            const doNow = !timer;

            timer = setTimeout(() => {
                timer = null;
            }, delay);
            
            if (doNow) {
                result = fn.apply(context, args);
            } 
        }
        else {
            timer = setTimeout(() => {
                fn.apply(context, args);
            }, delay);
        }

        return result;
    };
}
複製程式碼

但是這個實現方式有個缺點,因為除了第一次立即執行,其它情況都是在定時器中執行的,也就是非同步執行,返回值會是undefined。

考慮到非同步,我們也可以返回Promise:

function debounce(fn, delay, immediate) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        return new Promise((resolve, reject) => {
            timer && clearTimeout(timer);

            if (immediate) {
                const doNow = !timer;

                timer = setTimeout(() => {
                    timer = null;
                }, delay);

                doNow && resolve(fn.apply(context, args));
            }
            else {
                timer = setTimeout(() => {
                    resolve(fn.apply(context, args));
                }, delay);
            }
        });
    };
}
複製程式碼

如此,只要fn被執行,那必定可以拿到返回值!這也是防抖的終極版了!

下次聊聊防抖的兄弟-前端效能優化之節流-throttle

相關文章