32

防抖(debounce)

防抖的作用是将多个连续的debounced调用合并为一次func调用。作用见参考资料1。

  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。

防抖与节流之间的重要区别是,防抖是基于上次debounced调用来计算waitTime的;而节流是基于上次func调用来计算waitTime的,只要距离上次func调用超过了waitTime,就可以进行下次func调用。

实现2修改自参考资料2。个人认为实现1更好理解。

实现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

csRyan
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.