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

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

在日常搬磚中我們都會發(fā)現(xiàn)JS有很多高頻率觸發(fā)的事件,比如scroll职辨、mouseover、keydown之類的戈二。舉個實際例子舒裤,有個輸入框,在我們輸入的同時觉吭,希望向后端請求獲得自動補全的功能腾供,比如輸入“防”就可以提示“防抖節(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賦上一個定時器進行用戶的回調(diào)函數(shù)

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

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

這太容易了!讓我們看看lodash是怎么寫的防抖吧柜思,附上源碼(源碼略多)岩调,他里面還會import一些其他函數(shù),大致就是一些簡單的功能函數(shù)赡盘,比如now獲取當前時間号枕,toNumber轉(zhuǎn)換到數(shù)字,isObject判斷是不是對象(這個函數(shù)還是個錯的陨享,基于typeof寫并不能正確判斷數(shù)據(jù)類型葱淳,但是應該也沒人會把一個new String(xxx)塞進去當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é)流復用防抖代碼
            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ù)定硝,默認不執(zhí)行
            maxing = false,       // 是否有最大等待時間皿桑,默認沒有
            trailing = true;      // 是否在最后執(zhí)行一次回調(diào)函數(shù),默認執(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)用,防抖開始運作
        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需要存在)
                都是應該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)管毙,對應節(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ù)的包裝和閉包的應用裹虫,讓防抖和節(jié)流可以復用且不互相影響
  4. 按時間間隔制造定時器,沒有定時器的重復定義和消除的過程融击,通過 時間差定時器存在狀態(tài) 來判斷是否添加新的定時器任務
  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出去萄窜,可以進行進一步的操作
// 比如我們給onscroll="scroll加一個防抖"
let throttle_test = throttle(CB, 1000, option)

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

8.節(jié)流的代碼復用(利用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,  // 默認為true
            'maxWait': wait,     // maxWait為用戶設(shè)置的最大的等待時間衣摩,從而把防抖變成節(jié)流
            'trailing': trailing // 默認為true
        });
    }
  1. 兩種定時器創(chuàng)建方式(定時器到時炼鞠、maxing到時的強制觸發(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的我舍棄了什么(為了表達的清晰一些怔接,代碼沒有封裝復用)

  1. 因為不復用搪泳,防抖節(jié)流各司其職,不需要通過maxWait實現(xiàn)兩套邏輯
  2. 舍棄了option扼脐,換了個immediate岸军,代表回調(diào)是在一開始還是最后執(zhí)行,類似于leading與trailing的作用
  3. 舍棄了時間的判斷瓦侮,按照一開始的簡易思路艰赞,防抖粗暴的新建和清除定時器,節(jié)流則是通過定時器的有無肚吏,來判斷是否建立新的定時器方妖,思路更加直觀,但是從底層去思考罚攀,時間的判斷雖然不直觀党觅,但是性能上應該比建立定時器和清除定時器要好很多雌澄,畢竟人家是個完善的JS庫。杯瞻。镐牺。所以結(jié)論應該是我的防抖比lodash的要差,但是我的節(jié)流也沒有建立重復的定時器而且少了不必要的時間判斷性能還會好些魁莉?2墙А?旗唁!
  4. 沒有了maxWait畦浓,不能制造防抖和節(jié)流的結(jié)合體
  5. 沒有return result,其實逆皮。宅粥。。你也不見得用得到你自己回調(diào)函數(shù)的返回值电谣,一般都是進行一種行為秽梅,上傳個東西,打個點剿牺,拉個數(shù)據(jù)啥的
  6. 沒有暴露cancel和flush方法企垦,其實。晒来。钞诡。一般你也用不到這個功能,而且這倆函數(shù)很好寫湃崩,一個干掉定時器荧降,一個執(zhí)行回調(diào)(順手也可以干掉定時器),有需要加上即可~

完~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末朵诫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子剪返,更是在濱河造成了極大的恐慌邓梅,老刑警劉巖脱盲,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異日缨,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門乙各,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人幢竹,你說我怎么就攤上這事《骶玻” “怎么了焕毫?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵驶乾,是天一觀的道長。 經(jīng)常有香客問我疙咸,道長风科,這世上最難降的妖魔是什么撒轮? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任贼穆,我火速辦了婚禮,結(jié)果婚禮上顶瞳,老公的妹妹穿的比我還像新娘愕秫。我一直安慰自己,他們只是感情好戴甩,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著洲劣,像睡著了一般课蔬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上二跋,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機與錄音吞获,去河邊找鬼。 笑死各拷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的知市。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼嫂丙,長吁一口氣:“原來是場噩夢啊……” “哼规哲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起隅肥,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤糊秆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痘番,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡伍纫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年昂芜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片良漱。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡欢际,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出损趋,到底是詐尸還是另有隱情,我是刑警寧澤蒋失,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站篙挽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏铣卡。R本人自食惡果不足惜观腊,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望苫耸。 院中可真熱鬧,春花似錦褪子、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缨伊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刻坊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工徐块, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留灾而,地道東北人。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓铜犬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親癣猾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內(nèi)容