前端開發中的防抖和節流

 

啥是節流?

節流是保證在一段時間內,代碼只執行了一次。這個一段時間內指的是不管用戶操作了幾次,最終僅執行一次。比如說一個按鈕,用戶狂點按鈕,但是如果用節流技術的話,不管用戶點擊了幾次,最終某個時間段內只執行了一次代碼。這個時間段是可以自行設置,比如說每一秒執行一次。

啥是防抖?

防抖其實和節流有些類似,畢竟它們的最終目的都是如出一轍。防抖是在一段時間結束之後,才觸發一次事件。如果一段時間內未結束再次觸發了事件,那麼就會重新計算這段時間。同樣的例子,還是用戶狂點按鈕。但是僅在用戶停止點擊按鈕後的一段時間之後纔會執行一次。如果用戶暫停點擊按鈕的時間不到一段時間內又再次點擊按鈕,那麼就會重新計算時間。這個時間同樣可以自行設置。

爲啥要防抖或節流呢?

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

頁面的展示過程

展示過程大致爲以下順序:

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>

 

可以看到,用戶瘋狂點擊了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,以便做下一次節流的判斷。 看下效果:

 

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

<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的判斷條件都不滿足。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章