me

防抖节流


场景

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Document</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
    }
  </style>
</head>

<body>
  <div id="container"></div>
  <script>
    let count = 0
    let divContainer = document.getElementById("container")
    function getUserAction() {
      divContainer.innerHTML = count++
    }
    divContainer.onmousemove = getUserAction
  </script>
</body>

</html>

Tip

当鼠标一放到矩形框中,就会不停的触发事件 (如果是复杂的回调或者 ajax 的请求就会很频繁的触发事件)

有两种解决方案:防抖和节流

防抖

防抖的原理就是:尽管触发事件,但是我只触发最后一次事件,且 n 秒内不再触发。总之,就是要等你触发完事件 n 秒内不再触发事件

function debounce(func, wait) {
  let timeout;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func();
    }, wait);
  };
}
divContainer.onmousemove = debounce(getUserAction, 1000);

如果不使用箭头函数,就得考虑 this 绑定问题,还有事件对象event

function debounce(func, wait) {
  let timeout;
  let _self = this;
  return function (e) {
    clearTimeout(timeout);
    timeout = setTimeout(function () {
      Reflect.apply(func, _self, e);
    }, wait);
  };
}

立即执行

如果希望事件每次重新点击都会立刻执行,然后等到停止触发 n 秒后,才可以重新触发。而不是非要等到事件停止触发后才执行

function debounce(func, wait, immediate) {
  let timeout;
  return function (e) {
    //timeout 为 false 才会立即执行
    let hasImmediate = !timeout && immediate;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      //执行完定时器之后也会立即执行
      timeout = null;
    }, wait);
    if (hasImmediate) {
      func(e);
    }
  };
}

取消

function debounce(func, wait, immediate) {
  let timeout;
  function debounced(e) {
    let hasImmediate = !timeout && immediate;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      if (timeout) {
        func(e);
      }
      timeout = null;
    }, wait);
    if (hasImmediate) {
      func(e);
    }
  }
  debounced.cancel = () => {
    clearTimeout(timeout);
    timeout = null;
  };
  return debounced;
}

使用:click事件绑定一个监听器

let setUserAction = debounce(getUserAction, 10000, true);
divContainer.onmousemove = setUserAction;
document.getElementById("btn").addEventListener("click", function () {
  setUserAction.cancel();
});
document.getElement("btn").onclick = function () {
  setUserAction.cancel();
};

节流

Tip

持续触发事件,每隔一段时间,只执行一次事件

使用时间戳

当触发事件的时候,取出当前的时间戳,然后减去之前的时间戳 (一开始值为 0),如果大于设置的时间周期,然后更新时间戳为当前的时间戳。反之不执行

function throttle(func, wait) {
  let previous = 0;
  return function (e) {
    let now = new Date().getTime();
    if (now - previous > wait) {
      func(e);
      previous = now;
    }
  };
}

container.onmousemove = throttle(getuserAction, 1000);

使用定时器

当触发事件的时候,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行完,执行函数,清空定时器

function throttle(func, wait) {
  let timeout;
  return function (e) {
    timeout = setTimeout(() => {
      func(e);
      timeout = null;
    }, wait);
  };
}
container.onmousemove = throttle(getUserAction, 1000);

优化

如果鼠标移入能立刻执行,且停止触发的时候还能在执行一次

function throttle(func, wait, options) {
  let timeout, previous = 0;
  if (!options) options = {};
  let later = function () {
    privous = options.leading === fasle ? 0 : new Date().getTime();
    let previous = new Date().getTime();
    func();
    timeout = null;
  };
  function throttled() {
    let now = new Date().getTime();
    if (options.leading === false && !privous) privous = now;
    let remaining = wait - (now - previous);
    if (remaining < 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func();
    } else if (!timeout && options.trailing === true) {
      timeout = setTimeout(later, remaining);
    }
  }
  return throttled;
}

throttled 取消

throttled.cancel = function () {
  clearTimeout(timeout);
  timeout = null;
  previous = 0;
};