防抖和節流原理分析

窗口的resize、scroll、輸入框內容校驗等操作時,如果這些操作處理函數是較爲複雜或頁面頻繁重渲染等操作時,在這種情況下如果事件觸發的頻率無限制,會加重瀏覽器的負擔,導致用戶體驗非常糟糕。此時我們可以採用debounce(防抖)和throttle(節流)的方式來減少觸發的頻率,同時又不影響實際效果。

eg:搜索框的請求優化,輸入搜索詞條需要立即觸發搜索請求時,防抖和節流可以將多個請求合併爲一個請求

github筆記傳送門,走過路過 star 一個?

首先準備 html 文件中代碼如下:

<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
    var num = 1;
    var content = document.getElementById('content');

    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = count;
</script>

複製代碼

防抖

debounce(防抖),簡單來說就是防止抖動。

當持續觸發事件時,debounce 會合併事件且不會去觸發事件當一定時間內沒有觸發再這個事件時,才真正去觸發事件

非立即執行版

 

 

 

非立即執行版的意思是觸發事件後函數不會立即執行,而是在 n 秒後執行,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。

const debounce = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args)
    },wait);
  }
}
複製代碼

如此調用:

content.onmousemove = debounce(count,1000);
複製代碼

 

alt

 

 

立即執行版

 

 

 

立即執行版的意思是觸發事件後函數會立即執行,然後 n 秒內不觸發事件才能繼續執行函數的效果。

const debounce = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if (timeout) cleatTimeout(timeout);
    let callNow = !timeout;
    timeout = setTimeout(() => {
      timeout = null;
    },wait)
    
    if(callNow) func.apply(context,args)
   }
}
複製代碼

 

alt

 

 

結合版

/**
 * @desc 函數防抖
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param immediate true 表立即執行,false 表非立即執行
 */
function debounce(func,wait,immediate) {
    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}
複製代碼

節流

throttle(節流),當持續觸發事件時,保證隔間時間觸發一次事件。

持續觸發事件時,throttle 會合並一定時間內的事件,並在該時間結束時真正去觸發一次事件。

時間戳版

 

 

 

在持續觸發事件的過程中,函數會立即執行,並且每 1s 執行一次。

const throttle = (func, wait, ...args) => {
  let pre = 0;
  return function(){
    const context = this;
    let now = Date.now();
    if (now - pre >= wait){
       func.apply(context, args);
       pre = Date.now();
    }
  }
}
複製代碼

 

alt

 

 

定時器版

 

 

 

在持續觸發事件的過程中,函數不會立即執行,並且每 1s 執行一次,在停止觸發事件後,函數還會再執行一次。

const throttle = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if(!timeout){
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(context,args);
      },wait)
    }
  }
}
複製代碼

 

alt

 

 

結合版

其實時間戳版和定時器版的節流函數的區別就是,時間戳版的函數觸發是在時間段內開始的時候,而定時器版的函數觸發是在時間段內結束的時候。

/**
 * @desc 函數節流
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param type 1 表時間戳版,2 表定時器版
 */
function throttle(func, wait ,type) {
    if(type===1){
        var previous = 0;
    }else if(type===2){
        var timeout;
    }

    return function() {
        var context = this;
        var args = arguments;
        if(type===1){
            var now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(function() {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }

    }
}
複製代碼

underscore 源碼

/**
 * underscore 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      // 現在和上一次時間戳比較
      var last = _.now() - timestamp;
      // 如果當前間隔時間少於設定時間且大於0就重新設置定時器
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        // 否則的話就是時間到了執行回調函數
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      // 獲得時間戳
      timestamp = _.now();
      // 如果定時器不存在且立即執行函數
      var callNow = immediate && !timeout;
      // 如果定時器不存在就創建一個
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        // 如果需要立即執行函數的話 通過 apply 執行
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };
複製代碼
  • 對於按鈕防點擊來說的實現:一旦我開始一個定時器,只要我定時器還在,不管你怎麼點擊都不會執行回調函數。一旦定時器結束並設置爲 null,就可以再次點擊了。
  • 對於延時執行函數來說的實現:每次調用防抖動函數都會判斷本次調用和之前的時間間隔,如果小於需要的時間間隔,就會重新創建一個定時器,並且定時器的延時爲設定時間減去之前的時間間隔。一旦時間到了,就會執行相應的回調函數。
/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數的的調用,傳入{leading: false}。
 *                                如果想忽略結尾函數的調用,傳入{trailing: false}
 *                                兩者不能共存,否則函數不能執行
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 如果設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當前時間戳
      var now = _.now();
      // 首次進入前者肯定爲 true
	  // 如果需要第一次不執行函數
	  // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
 	  // 如果設置了 trailing,只會進入這個條件
	  // 如果沒有設置 leading,那麼第一次會進入這個條件
	  // 還有一點,你可能會覺得開啓了定時器那麼應該不會進入這個 if 條件了
	  // 其實還是會進入的,因爲定時器的延時
	  // 並不是準確的時間,很可能你設置了2秒
	  // 但是他需要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
	    // 沒有的話就開啓一個定時器
        // 並且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };


作者:goooooooogle
鏈接:

窗口的resize、scroll、輸入框內容校驗等操作時,如果這些操作處理函數是較爲複雜或頁面頻繁重渲染等操作時,在這種情況下如果事件觸發的頻率無限制,會加重瀏覽器的負擔,導致用戶體驗非常糟糕。此時我們可以採用debounce(防抖)和throttle(節流)的方式來減少觸發的頻率,同時又不影響實際效果。

eg:搜索框的請求優化,輸入搜索詞條需要立即觸發搜索請求時,防抖和節流可以將多個請求合併爲一個請求

github筆記傳送門,走過路過 star 一個?

首先準備 html 文件中代碼如下:

<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
    var num = 1;
    var content = document.getElementById('content');

    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = count;
</script>

複製代碼

防抖

debounce(防抖),簡單來說就是防止抖動。

當持續觸發事件時,debounce 會合併事件且不會去觸發事件當一定時間內沒有觸發再這個事件時,才真正去觸發事件

非立即執行版

 

 

 

非立即執行版的意思是觸發事件後函數不會立即執行,而是在 n 秒後執行,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。

const debounce = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args)
    },wait);
  }
}
複製代碼

如此調用:

content.onmousemove = debounce(count,1000);
複製代碼

 

alt

 

 

立即執行版

 

 

 

立即執行版的意思是觸發事件後函數會立即執行,然後 n 秒內不觸發事件才能繼續執行函數的效果。

const debounce = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if (timeout) cleatTimeout(timeout);
    let callNow = !timeout;
    timeout = setTimeout(() => {
      timeout = null;
    },wait)
    
    if(callNow) func.apply(context,args)
   }
}
複製代碼

 

alt

 

 

結合版

/**
 * @desc 函數防抖
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param immediate true 表立即執行,false 表非立即執行
 */
function debounce(func,wait,immediate) {
    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}
複製代碼

節流

throttle(節流),當持續觸發事件時,保證隔間時間觸發一次事件。

持續觸發事件時,throttle 會合並一定時間內的事件,並在該時間結束時真正去觸發一次事件。

時間戳版

 

 

 

在持續觸發事件的過程中,函數會立即執行,並且每 1s 執行一次。

const throttle = (func, wait, ...args) => {
  let pre = 0;
  return function(){
    const context = this;
    let now = Date.now();
    if (now - pre >= wait){
       func.apply(context, args);
       pre = Date.now();
    }
  }
}
複製代碼

 

alt

 

 

定時器版

 

 

 

在持續觸發事件的過程中,函數不會立即執行,並且每 1s 執行一次,在停止觸發事件後,函數還會再執行一次。

const throttle = (func, wait, ...args) => {
  let timeout;
  return function(){
    const context = this;
    if(!timeout){
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(context,args);
      },wait)
    }
  }
}
複製代碼

 

alt

 

 

結合版

其實時間戳版和定時器版的節流函數的區別就是,時間戳版的函數觸發是在時間段內開始的時候,而定時器版的函數觸發是在時間段內結束的時候。

/**
 * @desc 函數節流
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param type 1 表時間戳版,2 表定時器版
 */
function throttle(func, wait ,type) {
    if(type===1){
        var previous = 0;
    }else if(type===2){
        var timeout;
    }

    return function() {
        var context = this;
        var args = arguments;
        if(type===1){
            var now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(function() {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }

    }
}
複製代碼

underscore 源碼

/**
 * underscore 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      // 現在和上一次時間戳比較
      var last = _.now() - timestamp;
      // 如果當前間隔時間少於設定時間且大於0就重新設置定時器
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        // 否則的話就是時間到了執行回調函數
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      // 獲得時間戳
      timestamp = _.now();
      // 如果定時器不存在且立即執行函數
      var callNow = immediate && !timeout;
      // 如果定時器不存在就創建一個
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        // 如果需要立即執行函數的話 通過 apply 執行
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };
複製代碼
  • 對於按鈕防點擊來說的實現:一旦我開始一個定時器,只要我定時器還在,不管你怎麼點擊都不會執行回調函數。一旦定時器結束並設置爲 null,就可以再次點擊了。
  • 對於延時執行函數來說的實現:每次調用防抖動函數都會判斷本次調用和之前的時間間隔,如果小於需要的時間間隔,就會重新創建一個定時器,並且定時器的延時爲設定時間減去之前的時間間隔。一旦時間到了,就會執行相應的回調函數。
/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數的的調用,傳入{leading: false}。
 *                                如果想忽略結尾函數的調用,傳入{trailing: false}
 *                                兩者不能共存,否則函數不能執行
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 如果設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當前時間戳
      var now = _.now();
      // 首次進入前者肯定爲 true
	  // 如果需要第一次不執行函數
	  // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
 	  // 如果設置了 trailing,只會進入這個條件
	  // 如果沒有設置 leading,那麼第一次會進入這個條件
	  // 還有一點,你可能會覺得開啓了定時器那麼應該不會進入這個 if 條件了
	  // 其實還是會進入的,因爲定時器的延時
	  // 並不是準確的時間,很可能你設置了2秒
	  // 但是他需要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
	    // 沒有的話就開啓一個定時器
        // 並且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

 

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