1. 程式人生 > >函式節流與函式防抖的區別

函式節流與函式防抖的區別

函式節流與函式防抖是我們解決頻繁觸發DOM事件的兩種常用解決方案,但是經常傻傻分不清楚。。。這不,在專案中又用遇到了,在此處記錄一下

函式防抖 debounce

原理:將若干函式呼叫合成為一次,並在給定時間過去之後,或者連續事件完全觸發完成之後,呼叫一次(僅僅只會呼叫一次!!!!!!!!!!)。

舉個栗子:滾動scroll事件,不停滑動滾輪會連續觸發多次滾動事件,從而呼叫繫結的回撥函式,我們希望當我們停止滾動的時,才觸發一次回撥,這時可以使用函式防抖。

原理性程式碼及測試

// 給盒子較大的height,容易看到效果
<style>
    * {
        padding
: 0
; margin: 0; }
.box { width: 800px; height: 1200px; }
</style> <body> <div class="container"> <div class="box" style="background: tomato"></div> <div class="box" style="background: skyblue"></div> <div
class="box" style="background: red">
</div> <div class="box" style="background: yellow"></div> </div> <script> window.onload = function() { const decounce = function(fn, delay) { let timer = null return
function() { const context = this let args = arguments clearTimeout(timer) // 每次呼叫debounce函式都會將前一次的timer清空,確保只執行一次 timer = setTimeout(() => { fn.apply(context, args) }, delay) } } let num = 0 function scrollTap() { num++ console.log(`看看num吧 ${num}`) } // 此處的觸發時間間隔設定的很小 document.addEventListener('scroll', decounce(scrollTap, 500)) // document.addEventListener('scroll', scrollTap) }
</script> </body>

此處的觸發時間間隔設定的很小,如果勻速不間斷的滾動,不斷觸發scroll事件,如果不用debounce處理,可以發現num改變了很多次,用了debounce函式防抖,num在一次上時間的滾動中只改變了一次。

呼叫debouce使scrollTap防抖之後的結果:
使用debounce函式
直接呼叫scrollTap的結果:
直接觸發滾動的回撥

補充:瀏覽器在處理setTimeout和setInterval時,有最小時間間隔。
setTimeout的最短時間間隔是4毫秒;
setInterval的最短間隔時間是10毫秒,也就是說,小於10毫秒的時間間隔會被調整到10毫秒。
事實上,未優化時,scroll事件頻繁觸發的時間間隔也是這個最小時間間隔。
也就是說,當我們在debounce函式中的間隔事件設定不恰當(小於這個最小時間間隔),會使debounce無效。

函式節流 throttle

原理:當達到了一定的時間間隔就會執行一次;可以理解為是縮減執行頻率

舉個栗子:還是以scroll滾動事件來說吧,滾動事件是及其消耗瀏覽器效能的,不停觸發。以我在專案中碰到的問題,移動端通過scroll實現分頁,不斷滾動,我們不希望不斷髮送請求,只有當達到某個條件,比如,距離手機視窗底部150px才傳送一個請求,接下來就是展示新頁面的請求,不停滾動,如此反覆;這個時候就得用到函式節流。

原理性程式碼及實現

// 函式節流 throttle
// 方法一:定時器實現
const throttle = function(fn,delay) {
  let timer = null

  return function() {
    const context = this
    let args = arguments
    if(!timer) {
      timer = setTimeout(() => {
        fn.apply(context,args) 
        clearTimeout(timer) 
      },delay)
    }
  }
}

// 方法二:時間戳
const throttle2 = function(fn, delay) {
  let preTime = Date.now()

  return function() {
      const context = this
      let args = arguments
      let doTime = Date.now()
      if (doTime - preTime >= delay) {
          fn.apply(context, args)
          preTime = Date.now()
      }
  }
}

需要注意的是定時器方法實現throttle方法和debounce方法的不同:

**在debounce中:在執行setTimeout函式之前總會將timer用setTimeout清除,取消延遲程式碼塊,確保只執行一次
在throttle中:只要timer存在就會執行setTimeout,在setTimeout內部每次清空這個timer,但是延遲程式碼塊已經執行啦,確保一定頻率執行一次**

我們依舊可以在html頁面中進行測試scroll事件,html和css程式碼同debounce,此處不贅述,執行結果是(可以說是一場漫長的滾輪滾動了):
呼叫throttle函式

最後再來瞅瞅專案中封裝好的debounce和throttle函式,可以說是很優秀了,考慮的特別全面,希望自己以後封裝的函式也能考慮的這麼全面吧,加油!

/**
 * 空閒控制 返回函式連續呼叫時,空閒時間必須大於或等於 wait,func 才會執行
 *
 * @param  {function} func        傳入函式,最後一個引數是額外增加的this物件,.apply(this, args) 這種方式,this無法傳遞進函式
 * @param  {number}   wait        表示時間視窗的間隔
 * @param  {boolean}  immediate   設定為ture時,呼叫觸發於開始邊界而不是結束邊界
 * @return {function}             返回客戶呼叫函式
 */
const debounce = function(func, wait, immediate) {
    let timeout, args, context, timestamp, result;

    const later = function() {
        // 據上一次觸發時間間隔
        let last = Number(new Date()) - timestamp;

        // 上次被包裝函式被呼叫時間間隔last小於設定時間間隔wait
        if (last < wait && last > 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            // 如果設定為immediate===true,因為開始邊界已經呼叫過了此處無需呼叫
            if (!immediate) {
                result = func.call(context, ...args, context);
                if (!timeout) {
                    context = args = null;
                }
            }
        }
    };

    return function(..._args) {
        context = this;
        args = _args;
        timestamp = Number(new Date());
        const callNow = immediate && !timeout;
        // 如果延時不存在,重新設定延時
        if (!timeout) {
            timeout = setTimeout(later, wait);
        }
        if (callNow) {
            result = func.call(context, ...args, context);
            context = args = null;
        }

        return result;
    };
};
/**
 * 頻率控制 返回函式連續呼叫時,func 執行頻率限定為 次 / wait
 *
 * @param  {function}   func      傳入函式
 * @param  {number}     wait      表示時間視窗的間隔
 * @param  {object}     options   如果想忽略開始邊界上的呼叫,傳入{leading: false}。
 *                                如果想忽略結尾邊界上的呼叫,傳入{trailing: false}
 * @return {function}             返回客戶呼叫函式
 */
const throttle = function(func, wait, options) {
    let context, args, result;
    let timeout = null;
    // 上次執行時間點
    let previous = 0;
    if (!options) options = {};
    // 延遲執行函式
    let later = function() {
        // 若設定了開始邊界不執行選項,上次執行時間始終為0
        previous = options.leading === false ? 0 : Number(new Date());
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function(..._args) {
        let now = Number(new Date());
        // 首次執行時,如果設定了開始邊界不執行選項,將上次執行時間設定為當前時間。
        if (!previous && options.leading === false) previous = now;
        // 延遲執行時間間隔
        let remaining = wait - (now - previous);
        context = this;
        args = _args;
        // 延遲時間間隔remaining小於等於0,表示上次執行至此所間隔時間已經超過一個時間視窗
        // remaining大於時間視窗wait,表示客戶端系統時間被調整過
        if (remaining <= 0 || remaining > wait) {
            clearTimeout(timeout);
            timeout = null;
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
            //如果延遲執行不存在,且沒有設定結尾邊界不執行選項
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};