debounce and throttle

前言: 整個(gè)六月份就水了四篇文章,這完美的證明了我六月份基本沒(méi)學(xué)啥東西 ??。這周在工作中主要是寫(xiě)了消息系統(tǒng)模塊,自己封裝的,測(cè)試?yán)习l(fā)現(xiàn) bug ,只能發(fā)現(xiàn)一個(gè)修一個(gè),還好是小姐姐,不然都要過(guò)來(lái)揍我了,新項(xiàng)目可的仔細(xì)點(diǎn),多看 PRD 多測(cè)試。

主題:工作中我小伙伴喜歡用 lodash 這個(gè)庫(kù),我就喜歡 ES6 ,因?yàn)樵谖艺J(rèn)識(shí)中,ES6 處理數(shù)據(jù)已經(jīng)挺完美了,而且一個(gè)高手還能把處理數(shù)據(jù)的代碼全給壓平,代碼結(jié)構(gòu)由立體變扁平,類似這種:

//本人項(xiàng)目截取片段,寫(xiě)的不是很好多包涵
let unReadHeadIdArr = res.data.messages.map(item=>item && item.head.id);
let allMessArrId = window.sessionStorage.getItem("allMessVisable");
allMessArrId = allMessArrId ? JSON.parse(allMessArrId) : [];
let unReadHeadId = unReadHeadIdArr.filter(item=>!allMessArrId.includes(item));

其實(shí)我也是不想看 lodash,immediate這種第三方庫(kù),那么多 API,emmm,但是 ES6 也不是完美無(wú)缺的,其中就少了一個(gè)很重的東西,防抖和節(jié)流函數(shù)。而恰恰這兩個(gè)函數(shù)在項(xiàng)目中很常用到。這時(shí)候就能體驗(yàn)到 lodash 的便捷了,雖然它的代碼體積很大。最近我在寫(xiě)登陸模塊的按鈕的時(shí)候就體會(huì)很深,想起我以前技術(shù)差的時(shí)候能把項(xiàng)目模塊功能寫(xiě)出來(lái)就很 happy 了,現(xiàn)在開(kāi)始有點(diǎn)精力去注意這些邊邊角角了,that's brilliant。本著學(xué)習(xí)的態(tài)度,當(dāng)然的研究下 debounce and throttle 的源碼了,因?yàn)槿绻皇褂?lodash 第三方庫(kù)的時(shí)候,我們可以在項(xiàng)目中 util 文件夾中,放一些項(xiàng)目工具函數(shù)的地方,引入這兩個(gè)函數(shù),然后愉快的使用。

underscorelodashjs 這兩個(gè)庫(kù)都有 debounce and throttle 用法都差不多,就以 underscore 為例進(jìn)行研究吧。

先推薦一個(gè)視頻教程看看思路:手寫(xiě)函數(shù)防抖和節(jié)流——小馬哥_老師
還有一個(gè)文字版的教程也可以看看:underscore 函數(shù)去抖的實(shí)現(xiàn)

在去官網(wǎng)看看 debounce 和 throttle 的用法:

我了解的防抖和節(jié)流:

  • 防抖:多次觸發(fā),只執(zhí)行一次。
  • 節(jié)流:一直觸發(fā)期間合理執(zhí)行。

這是個(gè) underscore CDN 可以打開(kāi)對(duì)照源碼閱讀:underscore

一、基本骨架

鼠標(biāo)移動(dòng)無(wú)限次觸發(fā)

鼠標(biāo)移動(dòng)無(wú)限次觸發(fā)計(jì)數(shù)顯示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
        div{
            height: 300px;
            width: 900px;
            margin: 50px auto;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #fff;
            font-size: 38px;
            background-color: #222;
        }
    </style>
</head>
<body>
    <div id="app"></div>
    <script>
        let count = 0;
        app.onmousemove = function(){
            this.innerHTML = count++;
        }
    </script>
</body>
</html>

二、debounce 實(shí)現(xiàn)

underscore 中 debounce 函數(shù)有三個(gè)參數(shù):debounce(需要防抖的函數(shù),間隔時(shí)間,執(zhí)行順序)

首先講下實(shí)現(xiàn)的三個(gè)難點(diǎn):

  • 需要防抖函數(shù)中的 this;
  • 需要防抖函數(shù)的事件對(duì)象 event ;
  • 需要防抖函數(shù)返回結(jié)果不能改變

一版:能防抖、能綁定 this 和 event

<script>
    let count = 0;
    // debounce是一個(gè)高階函數(shù)
    function debounce(func,wait){
        let timeout,context;
        return function(...args){
            // 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
            //args就是事件對(duì)象event
            context = this;

            // 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
            if(timeout) clearTimeout(timeout);
            // 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
            timeout = setTimeout(function(){
                timeout = null;
                // func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
                func.apply(context,args);
            },wait);
        }
    }
    function wirteCount(){
        this.innerHTML = count++;
    }
    // debounce被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = debounce(wirteCount,1000);
</script>

二版:增加函數(shù)返回值
如果需要防抖的函數(shù) wirteCount 函數(shù)有返回值我們也應(yīng)該予以保留。

function wirteCount(){
    this.innerHTML = count++;
    return "我需要返回一些東西";
}

二版實(shí)現(xiàn),就增加了一個(gè) result 變量來(lái)接收 wirteCount 函數(shù)返回值:

<script>
    let count = 0;
    // debounce是一個(gè)高階函數(shù)
    function debounce(func,wait){
        let timeout,context,result;
        return function(...args){
            // 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
            //args就是事件對(duì)象event
            context = this;

            // 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
            if(timeout) clearTimeout(timeout);
            // 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
            timeout = setTimeout(function(){
                timeout = null;
                // func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
                result = func.apply(context,args);
            },wait);

            return result;
        }
    }
    function wirteCount(){
        this.innerHTML = count++;
        return "我需要返回一些東西";
    }
    // debounce被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = debounce(wirteCount,1000);
</script>

最最難的點(diǎn)來(lái)了,debounce 第三個(gè)參數(shù)的實(shí)現(xiàn),定義防抖函數(shù)剛觸發(fā)就執(zhí)行,還是觸發(fā)之后等 wait 秒在執(zhí)行。


三版:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
        section{
            margin: 50px auto;
            width: 900px;
            height: 300px;
        }
        div{
            height: 300px;
            width: 400px;
            float: left;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #fff;
            margin-left: 50px;
            font-size: 38px;
            background-color: #222;
        }

    </style>
</head>
<body>
    <section>
        <div id="app"></div>
        <div id="box"></div>
    </section>
    
<script>
    let count = 0;
    let idx = 0;
    // debounce是一個(gè)高階函數(shù)
    function debounce(func,wait,immediate){
        let timeout,context,result;
        return function(...args){
            // 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
            //args就是事件對(duì)象event
            context = this;

            // 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
            if(timeout) clearTimeout(timeout);

            if(immediate){
                // 第一次觸發(fā),timeout===undefined恰好可以利用timeout的值
                const callNow = !timeout;
                timeout = setTimeout(function(){
                    timeout = null;
                },wait);
                if(callNow) result = func.apply(context,args);

            }else{
                // 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
                timeout = setTimeout(function(){
                    timeout = null;
                    // func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
                    result = func.apply(context,args);
                },wait);
            }
            

            return result;
        }
    }
    function wirteCount(){
        if(this.id === "box"){
            this.innerHTML = count++;
        }else{
            this.innerHTML = idx++;
        }
        return "我需要返回一些東西";
    }
    // debounce被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = debounce(wirteCount,500,false);
    box.onmousemove = debounce(wirteCount,500,true);
</script>
</body>
</html>

四版:現(xiàn)在就差一個(gè)取消操作了,取消操作我們需要做些改變,需要把 debounce 函數(shù)返回的函數(shù)提取出來(lái)進(jìn)行擴(kuò)展。


2S內(nèi)可以取消事件執(zhí)行
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    // debounce是一個(gè)高階函數(shù)
    function debounce(func, wait, immediate) {
        let timeout, context, result;

        function resDebounced(...args) {
            // 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
            //args就是事件對(duì)象event
            context = this;

            // 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
            if (timeout) clearTimeout(timeout);

            if (immediate) {
                // 第一次觸發(fā),timeout===undefined恰好可以利用timeout的值
                const callNow = !timeout;
                timeout = setTimeout(function() {
                    timeout = null;
                }, wait);
                if (callNow) result = func.apply(context, args);

            } else {
                // 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
                timeout = setTimeout(function() {
                    timeout = null;
                    // func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
                    result = func.apply(context, args);
                }, wait);
            }
            return result;
        }
        resDebounced.cancal = function(){
            clearTimeout(timeout);
            timeout = null;
        }
        return resDebounced;
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些東西";
    }

    const implement = debounce(wirteCount, 2000, false);

    // debounce被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = implement;

    // 取消防抖
    btn.onclick = implement.cancal;
    </script>
</body>

</html>

debounce 到此就寫(xiě)完了,到此你能看懂幾乎所有第三方源碼實(shí)現(xiàn)了,因?yàn)樗鼈兊膶?shí)現(xiàn)基本都大同小異。

三、throttle 實(shí)現(xiàn)

你一定認(rèn)為 debounce 都實(shí)現(xiàn)了,throttle 就不是很難了,No no 。throttle 最難的是第三個(gè)參數(shù)的實(shí)現(xiàn)思路,先來(lái)看看 underscore 中 throttle 的用法,throttle 前兩個(gè)參數(shù)和 debounce 沒(méi)啥區(qū)別,區(qū)別在于第三個(gè)參數(shù)不是 boolean 值,而是一個(gè)對(duì)象_throttle(func,wait,{leading: true,trailing:true}) leading 表示事件觸發(fā)立即執(zhí)行 func ,trailing 表示最后離開(kāi)是否觸發(fā) func。兩個(gè)都默認(rèn)為 true。

前置知識(shí):
debounce 函數(shù)一樣,也有三個(gè)難點(diǎn):

  • 需要防抖函數(shù)中的 this,通過(guò) apply 綁定;
  • 需要防抖函數(shù)的事件對(duì)象 event ,通過(guò) apply 傳入;
  • 需要防抖函數(shù)返回結(jié)果不能改變

現(xiàn)在這個(gè)就比較簡(jiǎn)單了,通過(guò)這三個(gè)變量 let ctx, args, result; 完美接受實(shí)現(xiàn),下面主要關(guān)注實(shí)現(xiàn) throttle 第三個(gè)參數(shù)的實(shí)現(xiàn)。

3.1 leading 實(shí)現(xiàn)

leading :函數(shù)一觸發(fā)就立即執(zhí)行 func ,然后穩(wěn)定的間隔執(zhí)行 func ,最后一次離開(kāi)不執(zhí)行 func。

下面通過(guò)時(shí)間戳來(lái)實(shí)現(xiàn)的:

  1. 剛開(kāi)始 old = 0 條件 now - old > wait 一定為真,也就是 func 立即觸發(fā)。
  2. now - old > wait 第一次為真之后,func 就能穩(wěn)定執(zhí)行。
  3. 最后離開(kāi)不會(huì)執(zhí)行 func ,快速進(jìn)入快速離開(kāi),你會(huì)發(fā)現(xiàn) func 只在進(jìn)入執(zhí)行了一次。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    // throttle是一個(gè)高階函數(shù)
    function throttle(func,wait){
        let ctx,args,result;
        let old = 0;
        return function(){
            ctx = this;
            args = arguments;
            let now = Date.now();
            if(now - old > wait){
                result = func.apply(ctx,args);
                old = now;
            };

            return result;
        }
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些東西";
    }

    const implement = throttle(wirteCount, 1000);

    // throttle被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = implement;

    </script>
</body>

</html>

3.2 trailing 的實(shí)現(xiàn)

  • 第一次進(jìn)入不觸發(fā),然后穩(wěn)定的間隔執(zhí)行 func ,最后一次離開(kāi)執(zhí)行 func。
  • 快速進(jìn)入快速離開(kāi),你會(huì)發(fā)現(xiàn) func 只在離開(kāi)后執(zhí)行了一次。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    function throttle(func,wait){
        let ctx,args,result,timeout;
        return function(){
            ctx = this;
            args = arguments;
            if(!timeout){
                timeout = setTimeout(function(){
                    timeout = null;
                    result = func.apply(ctx,args);
                },wait);
            };
            return result;
        }
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些東西";
    }

    const implement = throttle(wirteCount, 1000);

    // throttle被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = implement;

    </script>
</body>

</html>

3.3 leading 和 trailing 二合一實(shí)現(xiàn) throttle

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    function throttle(func,wait,options = {}){
        let ctx, args, result, timeout, old = 0;
        let later = function(){
            result = func.apply(ctx,args);
            // 只要執(zhí)行func,old時(shí)間戳就的重置
            old = Date.now();
            timeout = null;
        }

        function resThrottle(){
            ctx = this;
            args = arguments;
            let now = Date.now();

            // 第一次觸發(fā)函數(shù)是否執(zhí)行
            if(options.leading === false && !old){
                old = now;
            }
            if(now - old > wait){
                // 當(dāng)條件now - old > wait為假時(shí),會(huì)開(kāi)啟延時(shí)器
                // 所以我們要清除下
                if(timeout){
                    clearTimeout(timeout);
                    timeout = null;
                }
                result = func.apply(ctx,args);
                old = now;
            }else if(!timeout && options.trailing !== false){
                timeout = setTimeout(later,wait);
            };

            return result;
        }

        resThrottle.cancal = function(){
            clearTimeout(timeout);
            old = 0;
            timeout = context = args = null;
        };

        return resThrottle;
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些東西";
    }

    const implement = throttle(wirteCount, 5000);

    // throttle被執(zhí)行必須返回一個(gè)函數(shù)
    app.onmousemove = implement;

    // 
    btn.onclick = implement.cancal;
    </script>
</body>

</html>

寫(xiě)作于北京昌平區(qū) 當(dāng)前時(shí)間 Saturday, June 27, 2020 02:29:33

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

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