彻底搞懂防抖(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)
相关推荐
嘿起屁儿整7 分钟前
面试点(网络层面)
前端·网络
Dev7z7 分钟前
基于 MATLAB 的铣削切削力建模与仿真
开发语言·matlab
不能隔夜的咖喱13 分钟前
牛客网刷题(2)
java·开发语言·算法
VT.馒头13 分钟前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
小天源20 分钟前
Error 1053 Error 1067 服务“启动后立即停止” Java / Python 程序无法后台运行 windows nssm注册器下载与报错处理
开发语言·windows·python·nssm·error 1053·error 1067
有位神秘人40 分钟前
Android中Notification的使用详解
android·java·javascript
肉包_5111 小时前
两个数据库互锁,用全局变量互锁会偶发软件卡死
开发语言·数据库·c++
大空大地20261 小时前
流程控制语句--if语句
开发语言
phltxy1 小时前
Vue 核心特性实战指南:指令、样式绑定、计算属性与侦听器
前端·javascript·vue.js
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于PHP的发热病人管理平台的设计与实现为例,包含答辩的问题和答案
开发语言·php