天天看點

【跟着大佬學JavaScript】之防抖

前言

在前端開發中會遇到一些頻繁的事件觸發,比如:

  1. window 的 resize、scroll
  2. mousedown、mousemove、mousewheel(滑鼠滾輪)
  3. keyup(彈起鍵盤)、keydown(按下鍵盤)、keypress(按下字元鍵盤)

    ……

想象一下視窗的resize事件或者是一個元素的onmouseover事件 - 他們觸發時,執行的非常迅速,并且觸發很多次。如果你的回調過重,你可能使浏覽器死掉。

這就是為什麼要使用防抖。

原理

防抖的原理:在wait時間内,持續觸發某個事件。第一種情況:如果某個事件觸發wait秒内又觸發了該事件,就應該以新的事件wait等待時間為準,wait秒後再執行此事件;第二種情況:如果某個事件觸發wait秒後,未再觸發該事件,則在wait秒後直接執行該事件。

通俗一點:定義wait=2000,持續點選按鈕,前後點選間隔都在2秒内,則在最後一次點選按鈕後,等待2秒再執行func方法。如果點選完按鈕,2秒後未再次點選按鈕,則2秒後直接執行func方法。

示例代碼

代碼一(根據原理)

定義函數debounce

根據表述,我們可以知道需要傳入參數:func、wait

實作代碼:

function debounce(func, wait = 500) {
    let timeout; // 定義定時器,wait秒後需要清除定時器
    return function () {
      // 如果再次觸發函數時,已有timeout,則清空銷毀目前timeout,再以新的事件重新設定定時器
      if (timeout) clearTimeout(timeout);
    
      timeout = setTimeout(function () {
        func();
        clearTimeout(timeout)
      }, wait);
    };
}      

代碼二(解決函數this指向)

我們之前的原函數指向哪,如果使用我們的 debounce 函數包裹後,也要将this指向正确的對象。

function debounce(func, wait = 500) {
    let timeout, context;
    return function () {
          context = this;
          // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
          if (timeout) clearTimeout(timeout);
        
          timeout = setTimeout(function () {
                func.apply(context);
                clearTimeout(timeout);
          }, wait);
    };
}      

代碼三(解決函數event對象)

JavaScript 在事件處理函數中會提供事件對象 event;

是以,也要考慮到保持原函數的event對象相同

式一:

function debounce(func, wait = 500) {
    let timeout, context, args;
    return function () {
          context = this;
          args = arguments;
          // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
          if (timeout) clearTimeout(timeout);
        
          timeout = setTimeout(function () {
                func.apply(context, args);
                clearTimeout(timeout);
          }, wait);
    };
}      

式二:

function debounce(func, wait = 500) {
    let timeout, context;
    return function (...args) {
          context = this;
          // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
          if (timeout) clearTimeout(timeout);
        
          timeout = setTimeout(function () {
                func.apply(context, args);
                clearTimeout(timeout);
          }, wait);
    };
}      

代碼四(函數傳回值)

此時需要注意一個問題,就是我們在執行原函數時可能有傳回值,我們需要處理debounce函數,在最後也要有相同傳回值。

這裡做出的處理,是将​

​func.apply(context, args)​

​​單獨拿出來,輸出原函數的​

​result​

​。

function debounce(func, wait = 500) {
    let timeout, context, result;
    
    function showResult(e1, e2) {
        result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
        return result;
    }
    
    return function (...args) {
        context = this;
        // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
        if (timeout) clearTimeout(timeout);
        
        // 這裡是不立即執行的原代碼
        timeout = setTimeout(function () {
            clearTimeout(timeout);
            return showResult(context, args); // 将this,arguments代入函數
        }, wait);
        return result
    };
}      

代碼五(立刻執行)

因為原理中,每次觸發完後還需要等待wait秒執行。

但是某些場景,比如按鈕點選後調用接口,會使整個時間變長,這時候就需要定義immediate,點選按鈕,立即執行調用接口,還要達到wait秒内防抖的效果。

function debounce(func, wait = 500, immediate = false) {
    let timeout, context, result, callNow;
    
    function showResult(e1, e2) {
        result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
        return result;
    }
    
    return function (...args) {
        context = this;
        // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 這裡是立即執行的判斷代碼
            callNow = !timeout; // timeout最開始定義為undefined,如果未設定定時器,則!timeout傳回true;否則傳回false
            timeout = setTimeout(function () {
                timeout = null; // 這裡時定時器走完,讓timeout為null,則上一步!timeout依然傳回true;
            }, wait);
            if (callNow) return showResult(context, args); //剛進入timeout=undefined以及,wait時間走完timeout = null,兩種情況都會立即執行函數
        } else {
        // 這裡是不立即執行的原代碼
            timeout = setTimeout(function () {
                
                clearTimeout(timeout);
                return showResult(context, args); // 将this,arguments代入函數
            }, wait);
        }
        return result
    };
}      

代碼六(取消)

增加取消防抖的方法:隻需要定義cancel方法,去除定時器,将初始變量全部設定為undefined。

function debounce(func, wait = 500, immediate = false) {
    let timeout, context, result, callNow;
    
    function showResult(e1, e2) {
        result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
        return result;
    }
    
    const debounced = function (...args) {
        context = this;
        // 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 這裡是立即執行的判斷代碼
            callNow = !timeout; // timeout最開始定義為undefined,如果未設定定時器,則!timeout傳回true;否則傳回false
            timeout = setTimeout(function () {
                timeout = null; // 這裡時定時器走完,讓timeout為null,則上一步!timeout依然傳回true;
            }, wait);
            if (callNow) return showResult(context, args); //剛進入timeout=undefined以及,wait時間走完timeout = null,兩種情況都會立即執行函數
        } else {
            // 這裡是不立即執行的原代碼
                timeout = setTimeout(function () {
                    return showResult(context, args); // 将this,arguments代入函數
                    clearTimeout(timeout);
                }, wait);
        }
        return result
    };
    
    debounced.cancel = function () {
        // 去除定時器,
        if (timeout !== undefined) {
            clearTimeout(timeout);
        }
        // 将初始變量全部設定為undefined
        timeout = context = result = callNow = undefined;
    };
    return debounced;
}      

示範位址

可以去​​Github倉庫​​檢視示範代碼

跟着大佬學系列

主要是日常對每個進階知識點的摸透,跟着大佬一起去深入了解JavaScript的語言藝術。

後續會一直更新,希望各位看官不要吝啬手中的贊。

❤️ 感謝各位的支援!!!

參考

  • ​​JavaScript專題之跟着underscore學防抖​​
  • ​​underscore.js​​
  • ​​深入淺出防抖函數 debounce​​

原文位址

繼續閱讀