JS專題之去抖函數

1、前言

爲何會有去抖和節流這類工具函數? html

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

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

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

2、簡單版去抖(debounce)

去抖(debounce),也叫防抖,那抖動指的是什麼呢?抖動意味着操做的不穩定性,你能夠理解成躁動症,安靜不下來~防抖的含義即是爲了防止抖動形成的結果不許確,等到穩定的時候再處理結果。瀏覽器

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

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

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

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

<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 變量用傳入的處理函數的屬性代替了而已。

3、當即執行

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

可是有一個問題,假如我想輸入框輸入內容時,第一個字輸完就請數據怎麼作? 你能夠理解爲,你能夠立刻開始說話,可是說完話後 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,閉包的知識,能夠翻看我以前分享的文章。

相關文章
相關標籤/搜索