通俗易懂了解函数的防抖和节流 1.前言 2.概念 3.代码实现 4.总结

在一次面试中被问到:“谈一谈js中函数的防抖和节流。”,当时菜鸡如我的内心:
通俗易懂了解函数的防抖和节流
1.前言
2.概念
3.代码实现
4.总结

只能弱弱的说一句没怎么了解过。后来找到工作后就将这件事抛在脑后,也没在深究。

就在前几天维护公司内部代码的时候,发现这样一个场景:当用户在创建东西时,会把用户输入的名字发往服务端校验是否重名,而当时的代码是监听了input输入框的onchange事件,只要用户一输入字符,就立即发出请求校验,这能忍?如果名字有100个字符发100次请求?用户没输完你校验个毛线啊!

不能忍!优化!必须优化!首先想到的优化思路是:当用户输完后我再发请求校验,但是我又不知道用户什么时候输完。那么可以这样,用户一直在输入时,我不请求,当用户停止输入3秒后我就认为此时用户已经输入完成,这时候再发请求校验,这样即可大大的降低请求次数,提高性能。

就在我沾沾自喜的拿着优化方案给Leader看的时候,Leader听完淡淡的说了一句:函数防抖和节流了解一下。

此时回过神来,原来这就是防抖啊。

2.概念

函数防抖和节流,都是控制事件触发频率的方法,通常用户优化性能。

2.1 函数防抖(debounce)最后一个人说了算

函数防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。

防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

简单的说,当一个动作连续触发,则只执行最后一次。

常见应用场景:

连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

2.2 函数节流(throttle) 第一个人说了算

函数节流,就是限制一个函数在一定时间内只能执行一次。

节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。

节流中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

常见应用场景:

间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交

2.3 直观理解

为了方便理解,我们首先通过画图感受一下三种环境(正常情况、函数防抖情况 debounce、函数节流 throttle)下,对于mousemove事件回调的执行情况。
通俗易懂了解函数的防抖和节流
1.前言
2.概念
3.代码实现
4.总结

竖线的疏密代表事件执行的频繁程度。可以看到,正常情况下,竖线非常密集,函数执行的很频繁。而debounce(函数防抖)则很稀疏,只有当鼠标停止移动时才会执行一次。throttle(函数节流)分布的较为均已,每过一段时间就会执行一次。

3.代码实现

为了说明问题,假设一个场景:鼠标滑过一个div,触发onmousemove事件,它内部的文字会显示当前鼠标的坐标。

<style>
    #box {
       1000px;
      height: 500px;
      background: #ccc;
      font-size: 40px;
      text-align: center;
      line-height: 500px;
    }
</style>

<div ></div>

<script>
  const box = document.getElementById('box')
  box.onmousemove = function (e) {
    box.innerHTML = `${e.clientX}, ${e.clientY}`
  }
</script>

效果如下:

通俗易懂了解函数的防抖和节流
1.前言
2.概念
3.代码实现
4.总结

3.1 函数防抖(debounce)

我们想要这样的效果:当鼠标持续移动时,不显示鼠标坐标,当鼠标停止移动1秒后再显示鼠标坐标。

分解一下需求:

  • 持续触发不执行
  • 不触发的一段时间之后再执行

那么怎么实现上述的目标呢?我们先看这一点:在不触发的一段时间之后再执行,那就需要个定时器呀,定时器里面调用我们要执行的函数,将arguments传入。

封装一个函数,让持续触发的事件监听是我们封装的这个函数,将目标函数作为回调(func)传进去,等待一段时间过后执行目标函数。

function debounce(func, delay) {
  return function() {
    setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

第二点实现了,再看第一点:持续触发不执行。我们先思考一下,是什么让我们的函数执行了呢?是上边的setTimeout。OK,那现在的问题就变成了持续触发,不能有setTimeout。这样直接在事件持续触发的时候,清掉定时器就好了。

// func是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(func, delay) {
  // 定时器
  let timeout = null;
  return function() {
    // 每次事件被触发时,都去清除之前的旧定时器,旧定时器的回调就不会执行。
    if(timer) {
        clearTimeout(timeout) 
    }
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

用法:

 box.onmousemove = debounce(function (e) {
    box.innerHTML = `${e.clientX}, ${e.clientY}`
  }, 1000)

效果:
通俗易懂了解函数的防抖和节流
1.前言
2.概念
3.代码实现
4.总结

说明:

这里debounce函数执行的结果是其内部return的function的调用。也就是说鼠标经过的事件监听实际上是这个被return的function,不断持续触发的是它,而debounce函数内部用闭包声明了一个timeout的定时器,由于闭包的存在,timeout会被挂载在window对象上,每次鼠标经过,都会先清除掉上次声明的timeout,直到最后一次鼠标经过,而它的timeout没有被清除,所以最后一次的定时器才会执行。

3.2 函数节流(throttle)

我们想要这样的效果:当鼠标持续移动时,不显示鼠标坐标,每隔一定的时间再显示鼠标坐标。

同样,我们再分解一下需求:

  • 持续触发并不会执行多次
  • 到一定时间再去执行

持续触发,并不会执行,但是到时间了就会执行。抓取一个关键的点:就是执行的时机。要做到控制执行的时机,我们可以通过一个开关,与定时器setTimeout结合完成。

函数执行的前提条件是开关打开,持续触发时,持续关闭开关,等到setTimeout到时间了,再把开关打开,函数就会执行了。

function throttle(func, delay) {
    let run = true
    return function () {
      if (!run) {
        return  // 如果开关关闭了,那就直接不执行下边的代码
      }
      run = false // 持续触发的话,run一直是false,就会停在上边的判断那里
      setTimeout(() => {
        func.apply(this, arguments)
        run = true // 定时器到时间之后,会把开关打开,我们的函数就会被执行
      }, delay)
    }
  }

用法:

box.onmousemove = throttle(function (e) {
  box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)

效果:
通俗易懂了解函数的防抖和节流
1.前言
2.概念
3.代码实现
4.总结

4.总结

防抖和节流巧妙地用了setTimeout,来控制函数执行的时机,优点很明显,可以节约性能,不至于多次触发复杂的业务逻辑而造成页面卡顿。

函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。

函数防抖关注一定时间连续触发,只在最后执行一次,而函数节流侧重于一段时间内只执行一次。
(完)