JS專題之去抖函數

一、前言

爲什麼會有去抖和節流這類工具函數?

在用戶和前端頁面的交互過程中,很多操作的觸發頻率非常高,比如鼠標移動 mousemove 事件, 滾動條滑動 scroll 事件, 輸入框 input 事件, 鍵盤 keyup 事件,瀏覽器窗口 resize 事件。

在以上事件上綁定回調函數,如果回調函數是一些需要大量計算、消耗內存、HTTP 請求、DOM 操作等,那麼應用的性能和體驗就會非常的差。

去抖和節流函數的根據思想就是,減少高頻率事件處理函數 handler 的執行頻率(注意是事件處理函數,不是事件回調函數),將多次事件的回調合併成一個回調來執行,從而優化性能。

二、簡單版去抖(debounce)

去抖(debounce),也叫防抖,那抖動指的是什麼呢?抖動意味着操作的不穩定性,你可以理解成躁動症,安靜不下來~防抖的含義便是爲了防止抖動造成的結果不準確,等到穩定的時候再處理結果。

比如在輸入事件,鼠標移動,滾動條滑動,鍵盤敲擊事件中,等到停止事件觸發,頻率穩定爲零後,纔開始執行回調函數,也就是所謂的沒有抖動後處理。

個人總結:去抖,就是事件觸發頻率穩定後,纔開始執行回調函數, 一連串的事件觸發,但只進行一次事件處理。

頻率就是單位時間觸發的次數,如果單位時間內,事件觸發超過一次,就只執行最後一次,如果單位時間內沒有觸發超過一次,那就正常執行。去抖分爲延遲執行和立即執行兩種思路。

看一個簡單版的去抖函數延遲執行實現:

<div>
    輸入框: <input type="text" id="exampleInput">
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    inputEl.oninput = debounce(ajax); // debouce 函數執行了,返回一個函數,該函數爲事件的回調函數

    // 事件真正的處理函數(handler),參數是回調函數傳遞過來的。
    // 常見場景就是邊輸入查詢關鍵字,邊請求查詢數據,比如百度的首頁搜索
    function ajax(event) {
        console.log("HTTP 異步請求:", event.target.value);
        // $.ajax() 請求數據 ...
    }

    function debounce(func, delay) {  // 參數爲傳入的事件處理函數和間隔時間
        var interval = delay || 1000;
        var timer = null;  // 閉包保存的 timer 變量,會常駐內存

        return function(args) { // 返回的匿名函數是事件的回調函數,在事件觸發時執行,參數爲 DOM 事件對象(event)

            var context = this; // 事件的回調函數中,this 指向事件的綁定的 DOM 元素對象(HTMLElement)
            
            console.log(timer);
            clearTimeout(timer); // 如果事件回調函數中存在定時器,則清空上次定時器,重新計時。如果間隔時間到後,處理函數自然就被執行了。
            timer = setTimeout(function() {
                func.call(context, args); // 定時器時間到後,執行事件真正的處理函數 handler
                // 執行的事件處理函數(handler),需要把調用對象 this 和事件對象 傳遞過去,就像沒被debounce處理過一樣
            }, interval)
        }
    }
}
</script>

上面代碼中我的註釋已經能夠說明整個去抖的過程,再來囉嗦幾句話~

  1. debounce 函數在主線程順序執行時已經被調用,傳入的參數一個是真正想在事件觸發執行的事件處理函數
  2. 另一個參數是事件觸發的間隔時間,間隔時間內再次觸發事件,則重新計時,類似於罰你 5 分鐘內不準說話,時間到後就可以開始說話,如果 5 分鐘內說話了,則再罰你 5 分鐘內不準說話,以此類推~
  3. debounce 函數有一個 timer 內部變量,timer 在返回的執行函數中被訪問,形成了閉包,有關閉包的內容,可以翻看我之前的文章《JavaScript之閉包》
  4. bebounce 函數返回的匿名函數纔是 input 事件的回調函數,所以該匿名函數有一個默認參數 event 對象。
  5. 同第 4 點,匿名函數是 dom 元素註冊事件的回調函數,所以匿名函數(回調函數)的 this 指向 HTMLInput 元素。
  6. 同第 2 點,觸發函數後,如果發現閉包中保存着 timer 變量, timer 變量初始值爲 null, 之後觸發定時器後,timer 爲當次定時器的 id,id 是一個數字。去抖的過程在於,如果在定時器的間隔時間內觸發了函數,它會把上一次事件觸發時定義的定時器清除,又重新定義一個定時器。如果本次定時器沒有被清除,時間到後就會自然執行事件處理函數。對這個過程有困惑的同學,可以把 timer 變量在 clearTimeout 之前打印出來就明白了。
  7. 延時執行了事件處理函數(handler),需要傳遞調用對象和事件對象過去,此處 call 可以和 apply 互換,如果用 apply, 傳遞 arguments 類數組即可。這樣保證了參數的一致性,就像沒被 debounce 處理過一樣。

以上就是去抖函數的基本思想, 可以參考示意圖

下面這張圖是高設 3 裏講的節流函數,其實是這一節所說的去抖函數,高設 3 將 timer 變量用傳入的處理函數的屬性代替了而已。

三、立即執行

第二節的簡單版去抖函數能滿足大部分只需要觸發一次事件處理的去抖場景:輸入數據查詢事件,下拉滾動條到窗口底部懶加載數據。

但是有一個問題,假如我想輸入框輸入內容時,第一個字輸完就請數據怎麼做? 你可以理解爲,你可以馬上開始說話,但是說完話後 5 分鐘不能說話,如果 5 分鐘內說話,則接下來再加 5 分鐘不能說話。如果 5 分鐘後沒說話, 那麼接下來,你又可以先說話,然後閉嘴 5 分鐘~

所以,引出來了立即執行版的去抖函數。

取消功能實現

<div>
    輸入框: <input type="text" id="exampleInput">
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    inputEl.oninput = debounce(ajax, 1000, true); // debouce 函數執行了,返回一個函數,該函數爲事件的回調函數

    // 事件真正的處理函數(handler),參數是回調函數傳遞過來的。
    function ajax(event) {

        console.log("HTTP 異步請求:", event.target.value);
    }

    function debounce(func, delay, immediate) {
        var interval = delay || 1000;
        var timer = null; // 定時器的初始值爲 null, 所以第一次觸發事件會立即執行,整個過程中 timer 充當了 flag 的作用,判斷能否立即執行(第一次或者上一次立即執行後超過了間隔時間)
        return function(args) {

            var context = this; // 事件的回調函數中,this 指向事件的綁定的 DOM 元素對象(HTMLElement)
            console.log(timer);
            clearTimeout(timer); // 每次有新事件觸發,都會清除之前的定時器,如果可以立即執行則執行,如果不可以立即執行則重新創建定時器。
            if (immediate) {
                // 如果上一次的 timer 不爲 null, 說明自上一次事件觸發並且立即執行處理函數後,間隔時間還未結束。所以 timer 本應爲數字 id,不爲 null!
                callNow = !timer;
                timer = setTimeout(function() {
                    timer = null; // 每次事件觸發,並在定時器時間超過後, 把定時器變量設置 null, 從而可以判斷出下一次是否能夠立即執行。
                }, interval);

                if (callNow) {
                    func.call(context, args);
                }
            } else {
                timer = setTimeout(function() {
                    func.call(context, args); // 定時器時間到後,執行事件真正的處理函數 handler
                }, interval)
            }

        }
    }
}
</script>

上面代碼的註釋,可以解釋整個流程,下面大致說一下:

  1. 非立即執行版本和前一節內容一樣,跳過。
  2. timer 初始值爲 null, 第一次觸發爲立即執行,!timer 爲 true, 所以能夠立即調用事件處理函數。
  3. 每次事件觸發, 都會把 timer 重新賦值,在間隔時間到之前 timer 爲數字 id, !timer 爲 false, 所以不能立即執行。如果間隔時間到了,會把當次事件觸發的定時器 id 置爲 null, 下一次事件觸發就能立即執行了。
  4. 朋友們可以通過觀察 timer 值的變化,思考整個過程,timer 在去抖的過程中充當 flag 的作用,可以用來判斷能否立即執行。

看看效果:

取消函數

假如去抖函數的間隔時間爲 5 秒鐘,我在這 5 秒鐘內又想立即執行可以怎麼做?於是我們給回調函數加個取消函數屬性。

函數也是一個對象,可以像其他一般對象那樣添加方法:

<div>
    輸入框: <input type="text" id="exampleInput"><button id="cancelBtn">取消</button>
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    var debouncedFunc = debounce(ajax, 5000, true); // 將事件處理函數經過去抖函數處理。
    inputEl.oninput = debouncedFunc; // 綁定去抖後的事件回調函數

    var cancelBtnEL = document.getElementById("cancelBtn");
    cancelBtnEL.onclick = debouncedFunc.cancel; // 綁定回調函數的屬性 cancel 方法,點擊頁面,重置去抖效果

    function ajax(event) {

        console.log("HTTP 異步請求:", event.target.value);
    }

    function debounce(func, delay, immediate) {
        var interval = delay || 5000;
        var timer = null;
        var revokeFunc = function(args) {
            var context = this;
            clearTimeout(timer);
            if (immediate) {

                callNow = !timer;
                timer = setTimeout(function() {
                    timer = null;
                }, interval);

                if (callNow) {
                    func.call(context, args);
                }
            } else {
                timer = setTimeout(function() {
                    func.call(context, args);
                }, interval)
            }

        }

        revokeFunc.cancel = function() {
            clearTimeout(timer); // 清空上一次事件觸發的定時器
            timer = null; // 重置 timer 爲 null, 從而下一次事件觸發就能立即執行。
        }

        return revokeFunc;
    }
}
</script>

看看效果:

總結

去抖函數的意義在於合併多次事件觸發爲一次事件處理,從而降低事件處理函數可能引發的大量重繪重排,http 請求,內存佔用和頁面卡頓。

另外,本文有關 this, call, apply,閉包的知識,可以翻看我之前分享的文章。

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