從lodash庫中窺探防抖與節(jié)流

1. 防抖與節(jié)流出現(xiàn)的背景

在日常搬磚中我們都會發(fā)現(xiàn)JS有很多高頻率觸發(fā)的事件,比如scroll、mouseover、keydown之類的。舉個實際例子,有個輸入框,在我們輸入的同時,希望向后端請求獲得自動補(bǔ)全的功能,比如輸入“防”就可以提示“防抖節(jié)流”,但是我們又不希望每次keydown或者input的change都發(fā)起一個新的請求,這時候我們就需要使用到防抖和節(jié)流了

2. 防抖節(jié)流的概念

防抖:設(shè)置一個時間間隔K秒,在K秒內(nèi)多次觸發(fā)事件,只會在最后一次事件結(jié)束后K秒觸發(fā)事件回調(diào),如果在最后一次事件結(jié)束不滿K秒的過程中再次觸發(fā)時間則會清除掉之前的定時器,并重新計時。
節(jié)流:設(shè)置一個時間間隔K秒,開始頻繁的觸發(fā)事件,事件每隔K秒便會觸發(fā)一次

3. lodash防抖節(jié)流實現(xiàn)

不考慮別人的庫是怎么實現(xiàn)的,最直觀的講如何實現(xiàn)防抖

第一步:閉包內(nèi) ||全局 || vue對象 之類的地方上定義一個隨時都能訪問到的變量timeout
第二步: 在事件觸發(fā)的時候,通過timeout判斷定時器是否存在,存在就clear掉,不存在就給timeout賦上一個定時器進(jìn)行用戶的回調(diào)函數(shù)

如何實現(xiàn)節(jié)流

第一步:閉包內(nèi) ||全局 || vue對象 之類的地方上定義一個隨時都能訪問到的變量timeout
第二步: 在事件觸發(fā)的時候,通過timeout判斷定時器是否存在,如果存在就啥都不干,如果不存在就加一個定時器

這太容易了!讓我們看看lodash是怎么寫的防抖吧,附上源碼(源碼略多),他里面還會import一些其他函數(shù),大致就是一些簡單的功能函數(shù),比如now獲取當(dāng)前時間,toNumber轉(zhuǎn)換到數(shù)字,isObject判斷是不是對象(這個函數(shù)還是個錯的,基于typeof寫并不能正確判斷數(shù)據(jù)類型,但是應(yīng)該也沒人會把一個new String(xxx)塞進(jìn)去當(dāng)option)

function debounce(func, wait, options) {
        var lastArgs,             // arguments暫存(因為arguments和this都是debounce函數(shù)接收的)
            lastThis,             // this暫存     (而最終調(diào)用卻是invokeFunc函數(shù),所以需要利用閉包暫存變量)
            maxWait,              // option中maxWait最大等待時間字段,代表超過這個時間,回調(diào)可以再次被觸發(fā),用于節(jié)流復(fù)用防抖代碼
            result,               // return出去的回調(diào)函數(shù)的返回值,回調(diào)有返回值&&回調(diào)被觸發(fā)才有值
            timerId,              // 定時器對象
            lastCallTime,         // 上次調(diào)用debounced的時間
            lastInvokeTime = 0,   // 最后一次調(diào)用用戶回調(diào)的時間
            leading = false,      // 是否立即執(zhí)行一次回調(diào)函數(shù),默認(rèn)不執(zhí)行
            maxing = false,       // 是否有最大等待時間,默認(rèn)沒有
            trailing = true;      // 是否在最后執(zhí)行一次回調(diào)函數(shù),默認(rèn)執(zhí)行

        if (typeof func != 'function') {
            throw new TypeError(FUNC_ERROR_TEXT);
        }

        wait = toNumber(wait) || 0;

        if (isObject(options)) {
            leading = !!options.leading;
            maxing = 'maxWait' in options;
            maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
            trailing = 'trailing' in options ? !!options.trailing : trailing;
        }

        // 綁定暫存的arguments和this,執(zhí)行用戶回調(diào),并返回函數(shù)的返回值
        function invokeFunc(time) {
            var args = lastArgs,
                thisArg = lastThis;

            lastArgs = lastThis = undefined;
            lastInvokeTime = time;
            result = func.apply(thisArg, args);
            return result;
        }

        // isInvoking為true && 沒有建立定時器時調(diào)用,防抖開始運(yùn)作
        function leadingEdge(time) {
            // Reset any `maxWait` timer.
            lastInvokeTime = time;
            // Start the timer for the trailing edge.
            timerId = setTimeout(timerExpired, wait);
            // Invoke the leading edge.
            return leading ? invokeFunc(time) : result;
        }

        // 計算需等待時間
        function remainingWait(time) {
            var timeSinceLastCall = time - lastCallTime,
                timeSinceLastInvoke = time - lastInvokeTime,
                timeWaiting = wait - timeSinceLastCall;

            return maxing
                ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
                : timeWaiting;
        }

        // 判斷是否能invoke,用于來判斷是否能 制造定時器 來執(zhí)行用戶的回調(diào)函數(shù)
        function shouldInvoke(time) {
            var timeSinceLastCall = time - lastCallTime,
                timeSinceLastInvoke = time - lastInvokeTime;

            /** 
                lastCallTime未定義||
                這次調(diào)用和上次調(diào)用的差不小于用戶傳入的間隔||
                該間隔小于0||
                設(shè)置的最大間隔時間存在,差值超過了最大間隔時間(為節(jié)流設(shè)計,maxing需要存在)
                都是應(yīng)該Invoke的狀態(tài)
            **/
            return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
                (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
        }

        // 等待時間結(jié)束后,如果能invoke,通過trailingEdge觸發(fā)用戶回調(diào),如果不能 計算剩余時間 再重置定時器
        function timerExpired() {
            var time = now();
            if (shouldInvoke(time)) {
                return trailingEdge(time);
            }

            timerId = setTimeout(timerExpired, remainingWait(time));
        }

        // 等待時間結(jié)束后的執(zhí)行邏輯 :
        // 清空定時器變量,如果 最后需要執(zhí)行回調(diào) && 暫存的arguments存在 執(zhí)行用戶回調(diào)
        function trailingEdge(time) {
            timerId = undefined;

            if (trailing && lastArgs) {
                return invokeFunc(time);
            }
            lastArgs = lastThis = undefined;
            return result;
        }

        // 暴露的接口,手動取消防抖
        function cancel() {
            if (timerId !== undefined) {
                clearTimeout(timerId);
            }
            lastInvokeTime = 0;
            lastArgs = lastCallTime = lastThis = timerId = undefined;
        }

        // 暴露的接口,如果定時器存在,直接觸發(fā)回調(diào)
        function flush() {
              return timerId === undefined ? result : trailingEdge(now());
        }

        // 防抖核心函數(shù)
        function debounced() {
            var time = now(), isInvoking = shouldInvoke(time);

            lastArgs = arguments;
            lastThis = this;
            lastCallTime = time;

            if (isInvoking) { // 一開始lastCallTime為undefined所以isInvoking為true
                if (timerId === undefined) {
                    return leadingEdge(lastCallTime); // 這個是初始狀態(tài)
                }
                if (maxing) { // 節(jié)流觸發(fā)方式之一:時間到了,重造定時器,并執(zhí)行回調(diào)
                    timerId = setTimeout(timerExpired, wait);
                    return invokeFunc(lastCallTime);
                }
            }
            if (timerId === undefined) { 
            /** 
                這段邏輯和正常(只觸發(fā)一次)的防抖無關(guān),對應(yīng)節(jié)流觸發(fā)方式之二:trailingEdge(定時器正常到時間)
                正常的trailingEdge不會新建新的定時器,所以需要在這里新建定時器
                兩種觸發(fā)方式互斥,通過觸發(fā)后影響isInvoking的狀態(tài)防止二次觸發(fā)
            **/
                timerId = setTimeout(timerExpired, wait);
            }
            return result;
        }
        debounced.cancel = cancel;
        debounced.flush = flush;
        return debounced;
    }

這一堆代碼相比之前極簡的防抖節(jié)流有什么區(qū)別都提供了什么功能?

  1. lodash防抖實現(xiàn)邏輯: 通過不斷更新now和lastCallTime,讓shouldInvoke始終返回false無法觸發(fā)回調(diào),直到事件不觸發(fā)不更新lastCallTime才能觸發(fā)回調(diào)
  2. lodash節(jié)流實現(xiàn)邏輯:通過option中maxWait的傳入,放寬了shouldInvoke的判定條件,使定時器能夠正常觸發(fā)用戶定義的回調(diào)函數(shù),并在事件的循環(huán)回調(diào)中加入maxing判斷邏輯,作為觸發(fā)用戶定義回調(diào)的第二入口,并通過影響isInvoking的狀態(tài)防止二次觸發(fā)
  3. 函數(shù)的包裝和閉包的應(yīng)用,讓防抖和節(jié)流可以復(fù)用且不互相影響
  4. 按時間間隔制造定時器,沒有定時器的重復(fù)定義和消除的過程,通過 時間差定時器存在狀態(tài) 來判斷是否添加新的定時器任務(wù)
  5. 靈活的參數(shù),通過leading和trailing來控制回調(diào)在一連串的事件行為的開始還是結(jié)束時被觸發(fā)
  6. lastArgs、lastThis讓函數(shù)之間通信更加便利
// 先定義一個防抖
let throttle_a = throttle(functionCB(){...}, waitTime),
// 然后再對其傳參
throttle_a(param1,...,paramN)
// 這些參數(shù)可以在functionCB中訪問
function functionCB () { 
    console.log(arguments) // 可以拿到上面的param1,...,paramN
}
  1. 平時為undefined的result會在回調(diào)執(zhí)行后,賦上functionCB的返回值,并被return出去,可以進(jìn)行進(jìn)一步的操作
// 比如我們給onscroll="scroll加一個防抖"
let throttle_test = throttle(CB, 1000, option)

function scroll () {
    let a = throttle_test()
    // 這里a會得到something,然后進(jìn)行相關(guān)操作
    ... doSomething with somethingFromCB
}
function functionCB () { 
    return somethingFromCB
}

8.節(jié)流的代碼復(fù)用(利用option中的maxWait打通debounced中if (maxing) {...}中的邏輯)

    function throttle(func, wait, options) {
        var leading = true, trailing = true;

        if (typeof func != 'function') {
            throw new TypeError(FUNC_ERROR_TEXT);
        }
        if (isObject(options)) {
            leading = 'leading' in options ? !!options.leading : leading;
            trailing = 'trailing' in options ? !!options.trailing : trailing;
        }
        return debounce(func, wait, {
            'leading': leading,  // 默認(rèn)為true
            'maxWait': wait,     // maxWait為用戶設(shè)置的最大的等待時間,從而把防抖變成節(jié)流
            'trailing': trailing // 默認(rèn)為true
        });
    }
  1. 兩種定時器創(chuàng)建方式(定時器到時、maxing到時的強(qiáng)制觸發(fā)),可以構(gòu)建出一種防抖和節(jié)流的結(jié)合體,比如wait定為1000,maxWait定為5000,也就是在5000ms內(nèi)連續(xù)折騰只能觸發(fā)一次,但是5000ms后又可以觸發(fā),使用更加靈活

4. 自己造一套簡易版的防抖節(jié)流

防抖:


function debounce (func, wait, immediate = true) {
    let timeout, _this, args;
    let later = () => setTimeout (() => {
        timeout = null
        if (!immediate) {
            func.apply(_this, args);
            _this = args = null;
        }
    },wait);

    let debounced = function (...params) {
        if (!timeout) {
            timeout = later();
            if (immediate){
                func.apply(this, params);
            } else {
                _this = this;
                args = params;
            }
        } else {
            clearTimeout(timeout);
            timeout = later();
        }
    }
    return debounced;
};

節(jié)流:

function throttle (func, wait, immediate = true) {
    let timeout, _this, args

    let later = () => setTimeout (() => {
        timeout = null
        if (!immediate) {
            func.apply(_this, args);
            _this = args = null;
        }
    }, wait);

    let throttled = function (...params) {
        if (!timeout) {
            timeout = later();
            if (immediate){
                immediate = false
                func.apply(this, params);
            }
            _this = this;
            args = params;
        }
     }
    return throttled;
};

相比lodash的我舍棄了什么(為了表達(dá)的清晰一些,代碼沒有封裝復(fù)用)

  1. 因為不復(fù)用,防抖節(jié)流各司其職,不需要通過maxWait實現(xiàn)兩套邏輯
  2. 舍棄了option,換了個immediate,代表回調(diào)是在一開始還是最后執(zhí)行,類似于leading與trailing的作用
  3. 舍棄了時間的判斷,按照一開始的簡易思路,防抖粗暴的新建和清除定時器,節(jié)流則是通過定時器的有無,來判斷是否建立新的定時器,思路更加直觀,但是從底層去思考,時間的判斷雖然不直觀,但是性能上應(yīng)該比建立定時器和清除定時器要好很多,畢竟人家是個完善的JS庫。。。所以結(jié)論應(yīng)該是我的防抖比lodash的要差,但是我的節(jié)流也沒有建立重復(fù)的定時器而且少了不必要的時間判斷性能還會好些????!
  4. 沒有了maxWait,不能制造防抖和節(jié)流的結(jié)合體
  5. 沒有return result,其實。。。你也不見得用得到你自己回調(diào)函數(shù)的返回值,一般都是進(jìn)行一種行為,上傳個東西,打個點(diǎn),拉個數(shù)據(jù)啥的
  6. 沒有暴露cancel和flush方法,其實。。。一般你也用不到這個功能,而且這倆函數(shù)很好寫,一個干掉定時器,一個執(zhí)行回調(diào)(順手也可以干掉定時器),有需要加上即可~

完~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容