JS之防抖节流

什么是防抖


防抖 (debounce),顾名思义就是防止抖动。一些高频触发的事件( 如:resizescrollmousemove…… )会导致事件处理函数高频执行。如果事件处理函数还操作DOM,那就意味着会引发高频的渲染重绘或回流。极端情况下,就会看到明显的页面/元素抖动。

所以,防抖就是防止高频的 DOM 操作导致页面频繁的渲染。

现代浏览器内核针对这种情况,内部进行了优化:会设置一个时间阀值,把这个时间内的 DOM 改变合并渲染。即便如此,我们在实际的项目中也要做好防抖,尽量为内核减负。

什么是节流


节流 (throttle),顾名思义就是节约流量(流量是网络世界宝贵的资源,web 又是流量的主要入口之一)。广义上也可以衍生为节约资源,这里的资源主要包括我们宝贵的浏览器内核资源。

所以,节流就是要避免不必要的网络请求,避免不必要的 js执行和页面渲染。

明显,广义上的节流是包含防抖的,只是,防抖和节流在具体实现上是有差异的。

防抖和节流的区别


防抖和节流都需要设置一个时长

  • 防抖:延时执行,并且时长内没有重复触发才会执行,否则重新计时。所以,最后(最新)一次事件必定响应。典型场景有:

    • 页面缩放( resize ): 页面缩放的时候,动态调整某些元素的大小。典型的防抖场景。
    • 搜索框联想( change ): 连续输入,触发多次搜索。一方面浪费资源,另一方面,如果先联想的结果后返回,那么显示就不是最新匹配的。
    • 文本编辑器( change ): 实时保存的文本编辑器。问题同上。
  • 节流:高频事件在 时长 内处理一次即可,一般是这段时间的第一次。典型场景有:

    • 元素拖拽/缩放( mousedown/mousemove ): 需要实时显示元素的位置/大小,但是频率也无需和事件触发频率一致。
    • 提交按钮( click ): 会发起网络请求的点击按钮。遇到暴力点击,不光有重复提交的问题,还会导致流量和浏览器内部资源大大浪费。

根据实际场景的需求来选择 防抖 还是 节流 。比如 元素拖拽/缩放,如果需求不要求过程只追求结果,就应该选择 防抖。

明显,我们可以通过时间戳定时器来控制。下面,我们来看看防抖节流如何具体实现。

防抖函数


防抖的关键在于延迟执行,所以推荐使用定时器

  • 基础版: 延迟 ms 毫秒执行,在这期间的其他重复请求不执行。
function debounce(func, ms) {
  let timeout;
  return function () {
      const context = this;
      const args = arguments;
      clearTimeout(timeout)
      timeout = setTimeout(function(){
        func.apply(context, args)
      }, ms);
  }
}
window.onmousemove = debouce(()=> console.log(1), 1000);
  • 进阶版:+ 首次请求立即执行。
function debounce(func, ms, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, ms)
      if (callNow) func.apply(context, args)
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, ms);
    }
  }
}

节流函数


节流的关键在于控制一段时间内只执行一次,所以时间戳和定时器都可以。区别是时间戳版触发是在时间段内开始的时候,而定时器版触发是在时间段内结束的时候。

  • 定时器版:每ms 毫秒只执行一次。

function throttle(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args)
      }, wait)
    }

  }
}
  • 时间戳版:每ms 毫秒只执行一次。

function throttle(func, wait) {
    var previous = 0;
    return function () {
      var now = Date.now();
      var context = this;
      var args = arguments;
      if (now - previous > wait) {
        func.apply(context, args);
        previous = now;
      }
    }
}

参考文档


节流(throttle)与防抖(debounce)