防抖和節流的實現

防抖(debounce)

防抖的做用是將多個連續的debounced調用合併爲一次func調用。做用見參考資料1。css

  1. 兩次debounced調用的間隔小於waitTime,則視爲連續的調用。
  2. 若是距離上次debounced調用已通過去了waitTime的時間,則說明該輪連續調用已經結束(進入穩定狀態)。這個時間點也被稱爲trailing edge。
  3. 在trailing edge之後的第一次debounced調用是下一輪連續調用的開始。固然,第一次debounced調用也是一輪連續調用的開始。這個時間點也被稱爲leading edge。
  4. immediate參數能夠控制是否在leading edge執行一次func調用。callAfterStable參數控制是否在trailing edge執行一次func調用。所以,func調用能夠放在連續調用開始時,也能夠放在結束時,也能夠都放。通常設置immediate = false,callAfterStable = true,將func調用放在連續調用結束時。
  5. 假設debounced的調用一直持續不斷,且相鄰間隔都小於waitTime,則意味着連續調用一直沒有結束,放在trailing edge的func調用一直不會執行。
function debounce(
  func,
  waitTime = 1000,
  immediate = false,
  callAfterStable = true
) {
  if (!immediate && !callAfterStable)
    throw new Error("immediate 和 callAfterStable 不能同時爲false"); // 不然func.apply永遠不會調用
  let timeout = null;
  const debounced = function(...args) {
    // timeout的值決定當前是否處於穩定狀態(已經通過waitTime沒有被調用了)
    // 若是已經存在一個定時器,說明如今是處於一輪連續調用當中(非穩定狀態),須要從新計時
    if (timeout) clearTimeout(timeout);
    // 不然,此時是leading edge。若是配置了immediate,此時要觸發func
    else if (immediate) func.apply(this, args);

    // trailing edge將在waitTime時間之後到來,進入穩定狀態(前提是這段時間內沒有被調用)
    timeout = setTimeout(() => {
      // 這個回調被執行時,說明已經通過waitTime沒有被調用了,進入穩定狀態
      timeout = null;
      // 此時是trailing edge。若是配置了callAfterStable,要觸發func
      if (callAfterStable) func.apply(this, args);
    }, waitTime);
  };
  // 使用者能夠調用這個函數,強行進入穩定狀態
  debounced.forceStabilize = function() {
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
    }
  };
  return debounced;
}

節流(throttle)

節流的做用是限制func調用的頻率(最多每waitTime調用一次)。做用見參考資料2。html

防抖與節流之間的重要區別是,防抖是基於上次debounced調用來計算waitTime的;而節流是基於上次func調用來計算waitTime的,只要距離上次func調用超過了waitTime,就能夠進行下次func調用。git

實現2修改自參考資料2。我的認爲實現1更好理解。github

實現1

// immediate傳入true,將在leading edge就第一次調用func
// 不然,將在 leading edge+waitTime 的時候才第一次調用func
function throttle(func, waitTime = 1000, immediate = true) {
  let timeout = null,
    // called表示自從上次func調用之後,是不是否有調用過throttled
    called,
    // 存儲上一次調用throttled時提供的args和this,用來在timeExpired時調用func
    lastArgs,
    lastThis;

  function timeExpired() {
    if (called) {
      func.apply(lastThis, lastArgs);
      called = false;
      timeout = setTimeout(timeExpired, waitTime);
    } else {
      // trailing edge
      // trailing edge不調用func了,
      // 由於在waitTime以前調用過了func,且自從那之後,throttled就沒有被調用過。
      timeout = null;
      // 釋放內存
      lastArgs = lastThis = null;
    }
  }

  function throttled(...args) {
    lastArgs = args;
    lastThis = this;

    if (!timeout) {
      // leading edge
      if (immediate) {
        func.apply(lastThis, lastArgs);
        called = false;
      } else {
        // !immediate時,leading edge下一次的timeExpired必須調用func
        // 不然,若是在(leading edge, leading edge + waitTime]這段時間內沒有調用過throttled,func一次也不會執行
        called = true;
      }
      timeout = setTimeout(timeExpired, waitTime);
    } else {
      called = true;
    }
  }

  throttled.cancle = function() {
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
      lastArgs = lastThis = null;
    }
  };

  return throttled;
}

實現2

function throttle(
  func,
  waitTime = 1000,
  immediate = true,
  callAfterStable = true
) {
  if (!immediate && !callAfterStable)
    throw new Error("immediate 和 callAfterStable 不能同時爲false"); // 下面會指出緣由
  let timeout = null,
    // 上一次調用func的時間
    previous = 0;
  const throttled = function(...args) {
    const now = Date.now();
    // immediate==false時,previous==0有特殊的含義:當前處於穩定狀態,本次調用throttled不當即觸發func
    // 阻止當即觸發func的方式:previous = now,至關於0秒前剛剛調用過了func
    // 所以穩定狀態下的第一次throttled調用會進入elseif,將func推遲調用
    if (!previous && !immediate) previous = now;
    const remain = waitTime - (now - previous);
    // immediate 和 callAfterStable 不能同時爲false,不然if和elseif語句塊都永遠不會調用
    if (remain < 0 || remain > waitTime) {
      // 距離上一次調用func至少通過了waitTime,本次throttled當即觸發func
      if (timeout) {
        // 有可能有timer回調仍阻塞在時間隊列中(雖然確定已經超時),銷燬它
        clearTimeout(timeout);
        timeout = null;
      }
      func.apply(this, args);
      previous = now;
    } else if (!timeout && callAfterStable) {
      // throttled調用時,距離上一次調用func尚未過去waitTime,
      // 不當即觸發func,而是安排到previous+waitTime時刻
      // 判斷!timeout是爲了防止安排多個func在previous+waitTime時刻調用
      timeout = setTimeout(() => {
        func.apply(this, args);
        // immediate==false時,previous=0表示進入穩定狀態,設置它是爲了阻止下一次的immediate調用
        previous = immediate ? Date.now() : 0;
        timeout = null;
      }, remain);
    }
  };
  throttled.forceStabilize = function() {
    previous = 0;
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
    }
  };
  return throttled;
}

測試代碼

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
  <title>test</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>

<body>
  <div id="container"></div>
  <script src="lib.js"></script>
  <script>
    var count = 1;
    var container = document.getElementById("container");

    function getUserAction() {
      container.innerHTML = count++;
    }

    // container.onmousemove = debounce(getUserAction);
    container.onmousemove = throttle(getUserAction);
  </script>
</body>

</html>

參考資料

  1. JavaScript專題之跟着underscore學防抖
  2. JavaScript專題之跟着underscore學節流
  3. Debouncing and Throttling Explained Through Examples
相關文章
相關標籤/搜索