我被面试问懵后,熬夜吃透了节流和防抖

10/18/2025 tag1

前段时间在面试前端时,面试官问我:"能手写一个带立即执行选项的防抖函数吗?"

我当场懵住了——这两个性能优化函数,咱们天天用在按钮点击、输入框搜索实时联想等事件里,却很少深究实现原理。

本文将结合我熬夜整理的节流和防抖案例,帮你全面重新认识节流和防抖,彻底吃透这两个前端必备技能。

# 认识节流机制

函数节流的核心思想是:事件触发后立即执行一次,然后在设定的时间周期内不再响应新的触发,直到周期结束后才重新开始接收事件。

就像咱们用的水龙头,拧紧之后水会一滴一滴均匀流出,而不是一直喷涌。用代码术语说,就是「把连续触发的事件按时间做平均分配执行」。

举个直观的例子:如果设定 2 秒为执行周期,第一次点击按钮会立即执行,接下来 2 秒内的所有点击都会被忽略,直到 2 秒后才允许再次执行。这种机制特别适合那些需要「匀速执行」的场景。

下面这个例子能清晰展示节流的效果——每 2 秒内不管点击多少次,只会执行一次:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>

  <body>
    <button id="throttle">测试函数节流</button>
  </body>

  <script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.js"></script>

  <script>
    // 处理点击事件的回调函数
    function handleClick(event) {
      console.log("处理点击事件", this, event);
    }

    /**
     * 使用 lodash 中的 throttle()
     * 参数一:指定要处理的事件
     * 参数二:延迟的时间(毫秒)
     * 参数三:配置对象
     */
    document.getElementById("throttle").onclick = _.throttle(
      handleClick,
      2000,
      {
        /**
         * trailing 设置为 false:则最后一次点击不会执行
         * 默认值为 true,则会在周期结束后执行最后一次
         */
        trailing: false,
      }
    );
  </script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 认识防抖机制

如果说节流像水龙头滴水,那防抖就像弹簧——你越是用力按压(频繁触发事件),它就越是要等你完全松手(停止触发)后才会反弹(执行函数)。

函数防抖(debounce) 的核心逻辑是:在事件触发后的一段时间内等待,如果在这段时间内没有再次触发事件,才执行函数。适用于那些需要等待一段时间后才执行的事件,以确保在连续触发事件时只执行一次。

举个直观的例子:比如说我设定了执行周期是 2 秒,那么我第一次点击之后,在后续的 2 秒内,如果我还有点击,那么就会更新最新的点击时刻,重新计算 2 秒,然后再去触发。

比如最常见的搜索框联想功能——用户输入过程中不需要频繁请求接口,只有当输入停顿超过 300 毫秒,才会触发搜索请求。这种"等待最后一次"的机制,完美解决了高频输入场景下的资源浪费问题。

同样我们来看一个防抖的代码案例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <button id="debounce">测试防抖函数</button>
  </body>

  <script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.js"></script>

  <script>
    function handleClick(event) {
      console.log("处理点击事件", this, event); // 该函数的this事件源, 即所绑定的dom元素
    }

    document.getElementById("debounce").onclick = _.debounce(handleClick, 2000);
  </script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在上面的代码中,如果从一开始就一直点击下去, 那么事件处理函数是不会执行的, 只有等到用户停止点击, 然后过了两秒后, 事件处理函数才会触发

# 节流和防抖典型使用场景

下面详细介绍两个典型应用场景,并深入分析如何选择和实现合适的优化方案。

# 防抖场景场景:搜索框输入联想优化

# 节流应用场景:窗口 resize 事件处理

# 节流函数实现

# 时间戳版实现(基础版)

其实 lodash 库的源码大致就是如下的实现方式

<body>
  <button id="throttle">测试函数节流</button>
</body>

<script>
  function throttle(callback, delay) {
    // 一上来,不点击按钮就会执行一次,有且仅会执行一次,后续点击按钮都不会执行
    console.log("hello");

    // pre不要设置成 Date.now() ,否则第一次点击的时候是不会调用的
    let pre = 0;
    return function (event) {
      // 这个才是真正的【节流函数】或者说是【真正的事件回调函数】, this 是触发事件的节点,即标签。

      // 这个函数会高频触发,因为是点击按钮,无法限制
      console.log("你点击按钮了");

      // 获取当前的时间
      const current = Date.now();
      if (current - pre > delay) {
        /**
         * 1. 节流:说白了,就是需要满足【上一次调用handleClick函数已经过了delay的时间差了】,然后才去调用 【handleClick函数】
         * 2. 【调用】真正【处理事件的回调函数】, 并且将真正【处理事件的回调函数,即下面的 handleClick 函数里的 this】中的 this
         * 3. button.onClick = function() { console.log(this) }  // this 是 button 按钮
         */
        callback.call(this, event);

        // 记录下,调用 handleClick 函数用了多长时间W
        pre = current;
      }
    };
  }

  // 处理点击事件的回调函数
  function handleClick(event) {
    console.log("处理点击事件", this, event);
  }

  document.getElementById("throttle").onclick = throttle(handleClick, 2000);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 定时器版实现(支持最后一次执行)

underscore 内部源码大致就是如下

function throttleByTimer(fn, interval) {
  let timer = null; // 定时器状态

  return function (...args) {
    // 如果没有定时器,说明可以执行
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行后清空定时器,允许下次触发
      }, interval);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 防抖函数实现

# 基础版实现(lodash 风格)

<body>
  <button id="debounce">测试防抖函数</button>
</body>

<script>
  function debounce(callback, delay) {
    return function (event) {
      // this 是事件源
      // 判断对象上是否有某个属性的方式如下:
      // 方式一:
      //  if (callback.timeoutId) {}  会去原型链找
      // 方式二:
      // callback.hasOwnProperty('timeoutId') ---- 效率更高,只会在自身找,不会到原型链上找. 普通对象可以使用 hasOwnProperty,根据原型链的查找过程
      if (callback.hasOwnProperty("timeoutId")) {
        clearTimeout(callback.timeoutId);
      }

      // 虽然我也不知道为什么要把timeoutId挂载到callback上面,那就先记住吧
      callback.timeoutId = setTimeout(() => {
        // 必须是箭头函数,否则下面的this就有问题
        callback.call(this, event); // 修改callback的this为事件源

        // 删除,标识 handleClick 已经处理完
        delete callback.timeoutId;
      }, delay);
    };
  }

  function handleClick(event) {
    console.log("处理点击事件", this, event);
  }

  document.getElementById("debounce").onclick = debounce(handleClick, 2000);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 写在最后

我是七小,一个沉迷前端技术的工程师 | AIGC 探索者 | 副业赚钱实践者。

希望本文分享的节流与防抖知识对你有所帮助。

如果你在学习过程中遇到疑问,或者有更好的实践经验,欢迎在评论区交流。

最后求个赞!你的每一个鼓励,都是我持续输出优质内容的动力~