防抖和节流的实现

防抖(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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值