防抖和节流

# 防抖和节流

啥叫防抖,啥叫节流?第一次听说是不是感觉不知道是啥玩意。

如果你用过window.onscroll事件,那这个对你来说就容易理解多了,话不多说,下文介绍。

防抖节流 是前端在优化性能问题上,经常使用的两种技术手段。比如 inputscrollresizemousemove 等事件,如果不加以控制,频繁的触发,无疑将会带来额外的性能开销,极端情况下,可能造成死机卡死现象。

所以简单来说就是防抖就相当于事件等待用户停止操作,节流就是冷却时间结束,执行业务函数。

先说一个常见的功能,很多网站会提供这么一个按钮:用于返回顶部。这个功能我写在了JS首页

这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求-- 监听浏览器滚动事件,返回当前滚条与顶部的距离

function showScrollTop() {
  var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条的位置:' + scrollTop);
}
window.onscroll  = showScrollTop

执行一下,你会发现,就稍微滑动一下,这个函数就掉用了十几次。如果这个函数是一段请求数据的业务代码,你想一想,得请求多少次,极其浪费浏览器性能!!!

# 防抖(debounce)

基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如300ms,然后:

  • 如果在300ms内没有再次触发滚动事件,那么就执行函数
  • 如果在300ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。

实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现:

<script>
  function debounce(fn, delay) { // 延迟执行fn,fn就是我们的业务代码
    let timeId = null // 用来取消定时器的,也就是意味着下一次滑动会重新延迟调用业务函数fn
    return function () {
      if (timeId) {
        // 我们把下面的取消定时器的代码注释掉,会发现,虽然fn延迟调用,但是确实一瞬间吊用了十几次
        clearTimeout(timeId);
      }
      timeId = setTimeout(fn, delay)
    }
  }
  function showScrollTop() {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    console.log('防抖后滚动条的位置:' + scrollTop);
  }
  // 当我们一直滚动,浏览器会一直帮我们调用,下面的函数,所以我们要在debounce里面传递业务函数和延迟
  // delay的话,就需要在debounce函数内返回一个函数,
  // 这个函数的目的就在于无论里滑动多少次都不会立即调用,而是会延迟delay毫秒
  window.onscroll = debounce(showScrollTop, 1000)
</script>

此时会发现,必须在停止滚动1秒以后,才会打印出滚动条位置。

到这里,已经把防抖实现了,现在给出定义:

  • 对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次。

# 节流(throttle)

继续思考,使用上面的防抖方案来处理问题的结果是:

  • 如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。

但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?

我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现 这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      #box1 {
        height: 800vh;
        width: 50%;
        background-color: aqua;
      }
    </style>
  </head>
  <body>
    <div id="box1"></div>
    <script>
      function showScrollTop() {
        var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log('节流后滚动条的位置:' + scrollTop);
      }
      // 当我们一直滚动,浏览器会一直帮我们调用,下面的函数,所以我们要在延迟函数里面传递业务函数和延迟
      // delay的话,就需要在debounce函数内返回一个函数,
      // 这个函数的目的就在于无论里滑动多少次都不会立即调用,而是会延迟delay毫秒
      function throttle(fn, delay) {
        let valve = true;
        return function () {
          if (!valve) {
            return false;// 时间delay未到,不执行fn
          } else {
            // 如果不设置微false,在下一个定时执行fn的时间内会大量的激活onscroll事件,
            //也就是执行throttle函数
            valve = false;
            setTimeout(() => {
              fn();
              //fn执行完以后,就相当于技能用完以后,开启冷却倒计时
              // valve = fales 就相当于在王者荣耀里面技能已经好了,你还没使用,一旦使用,就设置valve=true
              valve = true;
            })
          }
        }
      }
      window.onscroll = throttle(showScrollTop, 600)
    </script>
  </body>
</html>

# throttle 应用场景

函数节流有哪些应用场景?哪些时候我们需要间隔一定时间触发回调来控制函数调用频率?

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次 #21 (comment) (opens new window)

# debounce 应用场景

函数去抖有哪些应用场景?哪些时候对于连续的事件响应我们只需要执行一次回调?

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

# 小结

函数节流和函数去抖的核心其实就是限制某一个方法被频繁触发,而一个方法之所以会被频繁触发,大多数情况下是因为 DOM 事件的监听回调,而这也是函数节流以及去抖多数情况下的应用场景。至于函数节流和去抖方法的具体代码实现以及调用方式

# 联想

上面的防抖的思想,也让我在写项目当中想到了,因为在项目启动时添加修改,但是敲击一次键盘都会出现无效的更新,能不能写个插件使得这个更新可以在我们自定义的时间之后更新,这样再加上热更新,就能使我们很好的调试我们的代码了!!!