1. 程式人生 > >前端開發的函式防抖與函式節流

前端開發的函式防抖與函式節流

最近在開發中遇到一個問題,如果在很短的事件內連續點選同一個按鈕,按鈕的事件會觸發多次,在網上查了一下資料發現undescore.js 這個工具外掛,裡面提供了這樣兩個函式 debounce 和  throttle ;underscore1.8.2的文件是這樣的:

一、throttle(節流)

 _.throttle(function, wait, [options])  option的值是 {leading:false,trailing:false 建立並返回一個像節流閥一樣的函式,當重複呼叫函式的時候,最多每隔 wait毫秒呼叫一次該函式。對於想控制一些觸發頻率較高的事件有幫助。(愚人碼頭注:詳見:

javascript函式的throttle和debounce

預設情況下,throttle將在你呼叫的第一時間儘快執行這個function,並且,如果你在wait週期內呼叫任意次數的函式,都將盡快的被覆蓋。如果你想禁用第一次首先執行的話,傳遞{leading: false},還有如果你想禁用最後一次執行的話,傳遞{trailing: false}。

var throttled = _.throttle(updatePosition, 100);
$(window).scroll(throttled);

以上只是給出了用法,下面來解析一下原始碼:

// 獲取當前時間的毫秒數,當然你通過 +new Date()也可以直接獲取到當前時間毫秒數
var now = Date.now || function() {
    return new Date().getTime();
};
var throttle = function(func, wait, options) {
    
    var context, args, result;  // 上下文this, 函式func的引數, 函式func的執行結果
    var timeout = null;         // 定時器
    var previous = 0;           // 上次事件func的執行時間毫秒數 
 
    // 可以傳入的值是 options.leading,options.trailing,都是布林型別
    if (!options) options = {};
    
    // 這是一個定時器任務函式(可以先不看這個,看完下面的程式碼再來看這個函式)
    var later = function() {
      // 如果 options.leading 為 false , 上次事件執行時間賦值為0
      previous = options.leading === false ? 0 : now();
      // 定時器置為null
      timeout = null;  
      // 傳遞上下文引數並執行
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    // 返回一個函式
    return function() {
      // 儲存當前上下文
      context = this;
      // 儲存當前引數
      args = arguments;
      
      var now = now(); // 當前時間毫秒數
      // 更新執行func的時間previous,並禁用func第一次首先執行
      if (!previous && options.leading === false) previous = now;
      // 還剩下多少延遲時間
      var remaining = wait - (now - previous);

      // remaining <= 0已經足夠證明已經到達wait的時間間隔,但這裡還考慮到假如客戶端修改了系統時間則馬上執行func函式。
      if (remaining <= 0 || remaining > wait) {
        // 如果定時器存在,清除定時器
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        // 將當前時間重新賦值給previous
        previous = now;
        // 執行函式func,將結果儲存在result
        result = func.apply(context, args);
        // 如果定時器不存在就清除上下文,我認為這裡是為了防止閉包造成的記憶體洩漏
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 沒有定時器任務,而且不禁用最後一次呼叫函式
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

二、debounce(防抖)

 _.debounce(function, wait, [immediate])  返回 function 函式的防反跳版本, 將延遲函式的執行(真正的執行)在函式最後一次呼叫時刻的 wait 毫秒之後. 對於必須在一些輸入(多是一些使用者操作)停止到達之後執行的行為有幫助。 例如: 渲染一個Markdown格式的評論預覽, 當視窗停止改變大小之後重新計算佈局, 等等.

傳參 immediate 為 true, debounce會在 wait 時間間隔的開始呼叫這個函式 。(愚人碼頭注:並且在 waite 的時間之內,不會再次呼叫。)在類似不小心點了提交按鈕兩下而提交了兩次的情況下很有用。 (感謝 

@ProgramKid 的翻譯建議)

var lazyLayout = _.debounce(calculateLayout, 300);
$(window).resize(lazyLayout);

下面來解析一下原始碼:

// 獲取當前時間的毫秒數,當然你通過 +new Date()也可以直接獲取到當前時間毫秒數
var _now = Date.now || function () {
  return new Date().getTime();
};
var _debounce = function (func, wait = 300, immediate) {
  // 定時器,函式func的引數,上下文this, 上一個函式被返回的時間戳, 函式func的執行結果
  var timeout, args, context, timestamp, result;

  // 這是一個定時器任務函式(可以先不看這個,看完下面的程式碼再來看這個函式)
  var later = function() {
    // 該定時器時間生成的時間戳與上一函式的時間戳的差值
    var last = _now() - timestamp;
    // 差值在0~wait之間,就重新設定定時器,否則判斷是否已經執行過一次了,沒有執行過就執行一次該函式
    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) {
      result = func.apply(context, args);
      // 釋放變數
      context = args = null;
    }
    return result;
  };
};

以上是本人的見解,如果有錯誤,還請您在評論中留言。