彻底搞懂防抖(Debounce)与节流(Throttle):源码实现与应用场景

在前端开发中,resizescrollmousemove 等事件触发频率极高。如果不加限制,会引发严重的性能问题(如页面卡顿、服务器压力过大)。

防抖节流就是为了解决这个问题而生的两兄弟,虽然目的相同(减少执行频率),但策略完全不同。

1. 核心概念与区别

用一个生动的例子来区分它们:

维度 防抖 (Debounce) 节流 (Throttle)
核心思想 最后一个人说了算 按照时间表办事
生活比喻 坐电梯:只要有人进来,电梯就重新计时,直到没人进来了才关门运行。 公交车:不管人多拥挤,每隔 10 分钟发一班车。
执行规律 狂点 100 次,可能只执行 1 次(最后一次)。 狂点 100 次,可能执行 10 次(均匀分布)。
适用场景 只需要最终结果(如搜索框输入)。 需要持续的过程反馈(如滚动加载)。

2. 防抖 (Debounce)

2.1 原理

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则打断之前的计划,重新计时

2.2 应用场景

  • 搜索框输入 (Input Search):用户停止输入 500ms 后才发送请求,避免输一个字请求一次。
  • 窗口调整 (Window Resize):调整大小时不计算,调整完毕后才重新计算布局。
  • 文本编辑器保存:用户停笔后自动保存。

2.3 手写源码 (带 immediate 参数)

面试难点:如何实现"立即执行一次,然后开始防抖"?(比如点赞按钮,想要立刻变色,但防止后续重复点击)。

javascript 复制代码
/**
 * 防抖函数
 * @param {Function} func  需要执行的函数
 * @param {number} wait    等待时间 (ms)
 * @param {boolean} immediate 是否立即执行 (true: 立即执行, false: 延迟执行)
 */
function debounce(func, wait, immediate = false) {
  let timeout;

  return function (...args) {
    // 保存当前的 this 和 arguments,确保 func 执行时上下文正确
    const context = this;

    // 核心逻辑:如果在 wait 时间内再次触发,清除上一次的定时器
    if (timeout) clearTimeout(timeout);

    if (immediate) {
      // 如果 timeout 为 null,说明是第一次触发(或者上一个周期已经结束)
      const callNow = !timeout;
      
      // 设置定时器,wait 毫秒后将 timeout 置空,代表这一轮防抖结束
      // 注意:这里的 setTimeout 不执行 func,只负责重置状态
      timeout = setTimeout(() => {
        timeout = null;
      }, wait);

      // 只有符合条件才立即执行
      if (callNow) func.apply(context, args);
    } else {
      // 普通防抖:延迟执行
      timeout = setTimeout(() => {
        func.apply(context, args);
      }, wait);
    }
  };
}

3. 节流 (Throttle)

3.1 原理

规定在一个单位时间内,只能触发一次函数。稀释函数的执行频率。

3.2 应用场景

  • 滚动加载 (Scroll):监听滚动条位置,每隔 200ms 计算一次是否到底部。
  • DOM 元素拖拽 (Drag)mousemove 事件高频触发,每隔 50ms 更新一次位置即可。
  • 按钮连击:游戏中限制发射子弹的频率(攻速限制)。

3.3 手写源码 (时间戳 + 定时器版)

节流的实现通常有两种流派:

  1. 时间戳版:触发立即执行,最后一次停止触发后不再执行。(首触发)
  2. 定时器版:触发不立即执行,最后一次停止触发后还会再执行一次。(尾触发)

下面我们将两者结合,或者通过 immediate 参数控制。为了面试清晰,这里提供一个逻辑最清晰的切换版

javascript 复制代码
/**
 * 节流函数
 * @param {Function} func 需要执行的函数
 * @param {number} wait   间隔时间 (ms)
 * @param {boolean} immediate 是否立即执行 (true: 时间戳版, false: 定时器版)
 */
function throttle(func, wait, immediate = true) {
  let timeout;
  let previous = 0; // 记录上一次执行的时间戳

  return function (...args) {
    const context = this;
    const now = Date.now();

    if (immediate) {
      // === 立即执行版 (时间戳逻辑) ===
      // 如果当前时间 - 上次执行时间 > 等待时间,则执行
      if (now - previous > wait) {
        func.apply(context, args);
        previous = now; // 更新执行时间
      }
    } else {
      // === 延迟执行版 (定时器逻辑) ===
      // 如果定时器不存在,说明当前时间段内还没有安排任务
      if (!timeout) {
        timeout = setTimeout(() => {
          timeout = null; // 执行完清空,允许下一次调度
          func.apply(context, args);
        }, wait);
      }
    }
  };
}

高级补充 :最完美的节流(如 Lodash)通常是 时间戳定时器 的结合体,能够做到"开始时立即执行" 且 "结束时执行最后一次"。但在手写面试中,清晰地写出上面两种逻辑的区分通常已经满分。

4. 总结

记住这两个词:

  • 防抖 (Debounce) -> 延迟直到平静 (Delay until quiet)
  • 节流 (Throttle) -> 固定频率 (Fixed frequency)
相关推荐
Yue丶越1 小时前
【C语言】内存函数
c语言·开发语言
纵有疾風起1 小时前
【C++—STL】红黑树底层封装与set/map模拟实现
开发语言·c++·经验分享·面试·开源·stl
执笔论英雄1 小时前
【RL】async_engine 远离
java·开发语言·网络
Nicholas681 小时前
Dart锁机制之synchronized源码解析Lock、BasicLock、objectMakeLock、objectSynchronized(一)
前端
不会c嘎嘎1 小时前
【数据结构】红黑树详解:从原理到C++实现
开发语言·数据结构
pandarking1 小时前
[CTF]攻防世界:ics-05
开发语言·javascript·web安全·网络安全·ecmascript
武子康1 小时前
AI研究-133 Java vs Kotlin/Go/Rust/Python/Node:2025 详细对比分析 定位与取舍指南
java·javascript·python·golang·rust·kotlin·node
小帆聊前端1 小时前
深度解读虚拟列表:从原理到实战,解决长列表渲染性能难题
前端·javascript
在下历飞雨1 小时前
Kuikly基础之动画实战:让孤寡青蛙“活”过来
前端·ios·harmonyos