# 1.前言

发现这样一个场景:当用户在创建东西时,会把用户输入的名字发往服务端校验是否重名,而当时的代码是监听了input输入框的onchange事件,只要用户一输入字符,就立即发出请求校验,这是不符合要求的。如果名字有 100 个字符发 100 次请求,用户没输完是不能进行验证的!

# 2.概念

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

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

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

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

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

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

# 常见应用场景:

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

  • 手机号、邮箱验证输入检测
  • input 输入框实现模糊匹配功能,用户在不断输入值时,只需用户最后一次输入完,再发送请求,用防抖来节约请求资源
  • window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

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

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

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

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

常见应用场景:

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

  • 滚动加载,加载更多或滚到底部监听(监听滚动事件,比如是否滑到底部自动加载更多)
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交
  • 鼠标不断点击触发,mousedown 事件的执行(单位时间内只触发一次)

# 2.3 直观理解

为了方便理解,我们首先通过画图感受一下三种环境(正常情况、函数防抖情况 debounce、函数节流 throttle)下,对于mousemove事件回调的执行情况。

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

# 3.代码实现

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

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

<div id="box"></div>

<script>
  const box = document.getElementById("box");
  box.onmousemove = function(e) {
    box.innerHTML = `${e.clientX}, ${e.clientY}`;
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

效果如下:

# 3.1 函数防抖(debounce)

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

分解一下需求:

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

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

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

function debounce(func, delay) {
  return function() {
    setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}
1
2
3
4
5
6
7

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

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

用法:

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

效果:

说明:

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

# 3.2 函数节流(throttle)

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

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

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

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

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

示例 1:

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
5
6
7
8
9
10
11
12
13
14
15
16
17

效果:

示例 2:

/**
 * @method 节流;
 */
export const throttle = (func, delay) => {
  var prev = Date.now();
  return function() {
    var context = this;
    var args = arguments;
    var now = Date.now();
    if (now - prev >= delay) {
      func.apply(context, args);
      prev = Date.now();
    }
  };
};
//使用
<SearchCmpt
...do something
handleSearch={throttle(handleSearch, 1000)}//handleSearch,模糊查询函数
...do something
/>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 4.总结

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

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

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