彻底搞懂防抖(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)
相关推荐
caimouse4 小时前
reactos编码规范
c语言·开发语言
小雨下雨的雨5 小时前
井字棋AI机器人实现详解 - Minimax算法实战-鸿蒙PC Electron框架完成
前端·人工智能·算法·华为·electron·鸿蒙
xieliyu.8 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
CryptoPP9 小时前
快速对接东京证券交易所API数据:实战指南与代码示例
开发语言·人工智能·windows·python·信息可视化·区块链
ZC跨境爬虫9 小时前
跟着 MDN 学JavaScript day_7:数学运算与逻辑判断实战测试
开发语言·前端·javascript·学习·ecmascript
fangdengfu1239 小时前
ES分析系统各个服务日志占用量
java·前端·elasticsearch
凌云拓界9 小时前
文件管理:让AI安全操作你的电脑 ——CogitoAgent开发实战(三)
javascript·人工智能·架构·开源·node.js
凌云拓界10 小时前
联网能力:让AI看见更广阔的世界 ——CogitoAgent开发实战(四)
javascript·人工智能·架构·node.js·创业创新
阳区欠10 小时前
【LangChain】LLM基础介绍
开发语言·python·langchain
Jinkxs10 小时前
Java 跨域14-Java 与区块链(Hyperledger)集成
java·开发语言·区块链