剖析前端開發中的防抖和節流

  • 啥是節流?

節流是保證在一段時間內,代碼只執行了一次。這個一段時間內指的是無論用戶操做了幾回,最終僅執行一次。好比說一個按鈕,用戶狂點按鈕,可是若是用節流技術的話,無論用戶點擊了幾回,最終某個時間段內只執行了一次代碼。這個時間段是能夠自行設置,好比說每一秒執行一次。html

  • 啥是防抖?

防抖其實和節流有些相似,畢竟它們的最終目的都是一模一樣。防抖是在一段時間結束以後,才觸發一次事件。若是一段時間內未結束再次觸發了事件,那麼就會從新計算這段時間。一樣的例子,仍是用戶狂點按鈕。可是僅在用戶中止點擊按鈕後的一段時間以後纔會執行一次。若是用戶暫停點擊按鈕的時間不到一段時間內又再次點擊按鈕,那麼就會從新計算時間。這個時間一樣能夠自行設置。閉包

  • 爲啥要防抖或節流呢?

爲了優化高頻率事件,好比說onscroll滾動 oninput搜索框聯想 resize窗口大小變化 onkeydown onkeyup...等等。這些高頻率事件頗有可能致使頁面卡頓,影響用戶體驗。運用防抖和節流能夠有效下降代碼的執行頻率,從而解決高頻率事件的頁面卡頓問題。或許還有疑問,爲啥高頻事件就會致使頁面卡頓呢?
這就要從頁面的展現過程提及了。app

  • 頁面的展現過程

展現過程大體爲如下順序:函數

  • Javascript -> Style -> Layout -> Paint -> Composite

首先,Javascript階段會往頁面中添加一些DOM或動畫,而後到Style階段肯定每一個DOM應該用什麼樣式規則。在Layout階段佈局,最終肯定DOM顯示的位置和大小。在Paint階段進行DOM的繪製,它是在不一樣層上進行繪製。注意,樣式變化是重繪,佈局和位置變化是重排。重排必定致使重繪,重繪不必定致使重排。最後一個階段Composite進行渲染層合併。(因此作一些動畫效果儘可能用CSS3的transform等屬性,由於該屬性是脫離文檔流,不用合併渲染層的。)因而可知,若是觸發了不少高頻率的事件,就會致使頁面不停的肯定位置和大小 ,不停的重排重繪而且合併渲染層。因此致使頁面卡頓也能夠解釋了。佈局

接下來會用例子來一步步實現節流和防抖的原理。測試

  • 節流

首先 好比頁面上有個按鈕,用戶能夠點擊該按鈕。該按鈕上綁定了一個點擊事件,用戶能夠瘋狂點擊觸發該事件,確定結果就是瘋狂觸發該事件。目標是讓該按鈕無論用戶點擊的多快,最終該事件每秒僅執行一次。優化

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>throttle</title>
    <style>
    .btn{
        width: 250px;
        height: 60px;
        background-color: hotpink;
        color: #fff;
        display: block;
        text-align: center;
        line-height: 60px;
        cursor: pointer;
        border-radius: 10px;
    }
    </style>
</head>
<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        /* 按鈕綁定了一個事件 打印log */
        btn.addEventListener('click',logger);
    </script>
</body>
</html>

clipboard.png

能夠看到,用戶瘋狂點擊了20次,那麼該事件也理所固然的執行了20次,這顯然不是咱們想要的。
基礎版:動畫

<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000));

        function throttle(func, wait){
            /* 上次的時間戳 默認第一次0 */
            let pre = 0;
            return function(){
                let now = Date.now();
                /* 若是當前時間與上次時間的間隔大於wait */
                if(now - pre > wait){
                    func.apply(this,arguments);
                    pre = now;
                }
            }
        }
    </script>
</body>

爲了儘量的減小篇幅,把一些無用的代碼都刪除了。
定義一個throttle方法,該方法傳入了兩個參數,一個是要執行的事件,另外一個是間隔時間。該throttle方法是一個閉包的寫法,而且返回了一個函數。首先定義了上次的時間戳pre,pre默認第一次爲0。而後獲取到當前時間,用當前時間減去上次的時間戳也就是pre,若是這個差值大於了傳遞的時間間隔wait,也就代表能夠執行下一次的函數了。因此執行方法而且傳遞this和參數。並把當前時間賦給pre,以便作下一次節流的判斷。 看下效果:ui

clipboard.png

能夠看到,雖然瘋狂點擊按鈕,可是事件卻沒有瘋狂觸發,保持了每一秒執行一次的速度。也就達成了咱們的目標。
可是還有一個問題就是,我最後點擊按鈕的那次也應該延遲觸發最後一次的事件,可是結果並無。須要補上最後一次沒有觸發事件的問題,接下來優化它。
進階版:this

<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000,{trailing:true}));

        function throttle(func, wait, options){
            let pre = 0;
            /* 定義一個timeout定時器 */
            let timeout;
            return function(){
                let now = Date.now();
                if(now - pre > wait){
                    if(timeout){
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    func.apply(this,arguments);
                    pre = now;
                }else if(!timeout && options.trailing !== false){
                    /* 若是當前時間和上次️時間的間隔小於wait 而且trailing爲true */
                    timeout = setTimeout(later,wait-(now-pre));
                }
            }
            function later(){
                func.apply(this,arguments);
            }
        }
    </script>
</body>

很明顯看到,進階版多傳了一個參數對象,trailing:true。該參數用來表示是否執行最後一次觸發的方法。
在函數中,首先定義了一個空的定時器變量timeout,用來計算時間間隔。其次多了一個else if的條件判斷,判斷若是時間間隔小於wait,就表示該方法要保留起來延遲去執行。因此生成了一個定時器,延遲執行later函數,later函數就是執行該func函數。此處注意一點,這個延遲時間的問題。延遲時間不能是wait,必須是wait減去當前時間和上次時間的時間獎額。剩下的纔是剩餘時間延遲。還有一點要注意,在if中必定要清楚定時器,否則會影響else if的條件判斷。通過測試,確實能在點擊的最後一次後,延遲不到一秒觸發了該事件。
剩下最後一個優化點,其實第一次點擊按鈕,也應該延遲觸發事件。目前的版本是點擊按鈕的第一次就直接觸發該事件。優化它:

最終版:

<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',throttle(logger,1000,{leading:false}));

        function throttle(func, wait, options){
            let pre = 0;
            let timeout;
            let now = Date.now();
            
            /* leading爲false 把當前時間賦給上次時間pre */
            if(!options.leading) pre = now;

            return function(){
                if(now - pre > wait){
                    if(timeout){
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    func.apply(this,arguments);
                    pre = now;
                }else if(!timeout && options.trailing !== false){
                    timeout = setTimeout(later,wait-(now-pre));
                }
            }
            function later(){
                /* 若是leading爲false 校訂pre時間爲0 */
                pre = options.leading===false?0:Date.now();
                func.apply(this,arguments);
            }
        }
    </script>
</body>

能夠看到,傳遞一個新的參數對象leading爲false。用來表示第一次也延遲執行。那麼問題來了,怎樣才能第一次延遲執行呢?實現其實很簡單,進階版已經實現了else if延遲執行,現只需讓第一次不走if,走else if就可實現第一次的延遲執行。總共改動僅兩處,第一處:判斷用戶是否傳遞了參數leading爲false。若是傳遞了leading爲false,則把當前時間now賦給上次時間pre。爲什麼這樣作呢? 目的就是爲了第一步的時候也走else if。這麼看。pre=now 那麼if判斷條件就至關與now-now。now-now=0,固然不知足if條件,即第一次走了else if。這還不算完,在else if中要校訂pre時間。若是option.leading爲false,那麼pre就初始爲0。pre爲0的就會走if。只有走了if纔會清空定時器,否則的話只會執行一次便不會繼續往下執行。由於if和else if的判斷條件都不知足。👌,節流到此爲止,接下來是防抖。

  • 防抖

基礎版:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>debounce</title>
    <style>
            .btn{
                width: 250px;
                height: 60px;
                background-color: hotpink;
                color: #fff;
                display: block;
                text-align: center;
                line-height: 60px;
                cursor: pointer;
                border-radius: 10px;
            }
            </style>
</head>
<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',debounce(logger,1000));

        function debounce(func, wait){
            let timeout;
            return function(){
                clearTimeout(timeout);
                timeout = setTimeout(()=>{
                    func.apply(this,arguments)
                },wait);
            }
        }
    </script>
</body>
</html>

和節流相似,也是一個按鈕,按鈕上綁定事件。經過debounce函數,傳入了func和wait。首先,debounce函數也是一個閉包寫法,並返回了一個函數。該函數作了兩件事。第一,清除上次的定時器。第二,執行並定時器並把wait延遲時間傳進去(定時器中執行了func函數)。至此,便可以實現防抖功能。

clipboard.png

能夠看到,無論瘋狂點擊了多少次。僅僅執行了最後的那一次。只有當時間間隔大於1秒後,纔有機會觸發下一個函數。可是咱們不只僅知足於此,若是用戶第一次點擊的時候就想立刻執行一次,接下來的點擊才延遲執行呢?實現它:
最終版:

<body>
    <btn class="btn">按鈕</btn>
    <script>
        let btn = document.getElementsByClassName('btn')[0];
        function logger(){
            console.log('log');
        }
        btn.addEventListener('click',debounce(logger,1000,true));

        function debounce(func, wait, firstRun){
            let timeout;
            return function(){
                clearTimeout(timeout);

                if(firstRun){
                    func.apply(this,arguments);
                    firstRun = false;
                }else{
                    timeout = setTimeout(()=>{
                        func.apply(this,arguments)
                    },wait);    
                }
            }
        }
    </script>
</body>

首先看到參數的變化,debounce多了第三個參數firstRun。參數firstRun是第一次是否延遲執行的標識。true表示第一次當即執行。反之,延遲執行。debounce函數體中多了一個條件判斷if。首先判斷了第三個參數是否爲true,爲true就當即執行func並把this綁定把參數傳遞進去。而且,很重要一點,執行完func後把firstRun置爲false。這樣以後的執行都會走else if。else if就是正常的延遲執行。看下效果吧:

clipboard.png

能夠看到。當點擊按鈕的第一次就立刻觸發了函數,以後的瘋狂點擊暫停後也僅僅執行了一次。效果達成~

ok,至此。咱們分別實現了防抖和節流。喜歡點個👍,thx~

相關文章
相關標籤/搜索