🚀别再让键盘“打喷嚏”了!防抖 & 节流:前端性能优化的双子星

38 阅读4分钟

“你敲得快,不代表我得跑得快。” —— 一个被频繁触发事件折磨到崩溃的 AJAX 请求


在前端开发的世界里,有些事情就像打喷嚏——一旦开始,就停不下来。比如用户疯狂地敲击键盘、疯狂地滚动页面、疯狂地点按钮……而我们的代码呢?只能傻乎乎地跟着一起“打喷嚏”,结果服务器压力山大,页面卡成 PPT。

这时候,防抖(Debounce)节流(Throttle) 就像两位冷静的“医生”,用闭包+定时器的魔法,帮我们优雅地“止住喷嚏”。

今天,我们就来聊聊这两位前端界的“双子星”,看看它们是如何拯救我们的性能于水火之中的!


🤔 问题来了:为什么我们需要防抖和节流?

想象一下这个场景:

用户在百度搜索框里输入“JavaScript”,但手速飞快,0.1 秒敲一个字。
如果每次 keyup 都发一次 AJAX 请求,那短短一秒内就会发出 10 次请求!
结果:服务器哭晕在厕所,用户却只关心最后一个词:“JavaScript”。

这就是典型的高频事件触发问题
常见的“喷嚏制造机”包括:

  • keyup / input(搜索建议)
  • scroll(滚动加载)
  • resize(窗口调整)
  • mousemove(拖拽、画图)
  • 点击按钮(防止重复提交)

如果不加控制,这些事件会像帕金森患者的手一样——抖个不停 😅


🔧 防抖(Debounce):只认“最后一哆嗦”

核心思想:不管你怎么触发,我只执行最后一次。

🎯 适用场景

  • 搜索建议(如百度、GitHub 的搜索框)
  • 表单校验(用户停止输入后再验证)
  • 窗口 resize 后重新计算布局

💡 原理

利用 setTimeout + 闭包保存定时器 ID。
每次触发事件时,先清除之前的定时器,再重新计时。
只有当用户“安静”一段时间后,才真正执行函数。

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

✅ 闭包的作用:让 timer 在多次调用中保持“记忆”,实现状态共享。


⏱️ 节流(Throttle):我有我的节奏,别催!

核心思想:无论你多急,我每隔 X 毫秒才执行一次。

🎯 适用场景

  • 滚动加载(无限滚动)
  • 按钮点击(防止连点)
  • 游戏中的技能冷却(比如 FPS 射速限制)

💡 原理

记录上次执行时间,只有当前时间与上次间隔超过设定值,才允许执行。

function throttle(fn, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

🎮 类比:就像 CS 里按住鼠标左键,子弹也不是无限发射——它有射速限制!


🆚 防抖 vs 节流:到底怎么选?

特性防抖(Debounce)节流(Throttle)
执行时机事件停止后执行固定间隔执行
是否保证执行只执行最后一次保证定期执行
适合场景输入完成、窗口停止缩放滚动、点击、游戏帧率

🧠 记忆口诀:
“防抖等你停,节流按钟行。”


🛠️ 实战演示:三个输入框的“命运”

来看一段经典对比代码(你也可以直接复制运行):

<input id="undebounce" placeholder="普通输入(疯狂打喷嚏)" />
<input id="debounce" placeholder="防抖输入(冷静到最后)" />
<input id="throttle" placeholder="节流输入(稳如老狗)" />

<script>
function ajax(content) {
  console.log('🚀 发送请求:', content);
}

// 防抖:500ms 内只执行最后一次
const debounceAjax = debounce(ajax, 500);

// 节流:每 500ms 最多执行一次
const throttleAjax = throttle(ajax, 500);

document.getElementById('undebounce').addEventListener('input', e => ajax(e.target.value));
document.getElementById('debounce').addEventListener('input', e => debounceAjax(e.target.value));
document.getElementById('throttle').addEventListener('input', e => throttleAjax(e.target.value));
</script>

试试快速输入“hello world”:

  • 第一个框:控制台刷屏!
  • 第二个框:等你松手才打印一次。
  • 第三个框:每隔 500ms 打印一次,稳得一批。

💎 闭包:幕后英雄

你可能注意到了,无论是防抖还是节流,都用到了闭包

为什么?

因为我们需要在多次函数调用之间共享状态(比如 timerlastTime),而不能污染全局变量。

闭包就像一个“私人保险箱”,把状态安全地藏起来,只给返回的函数使用。

🙌 没有闭包,就没有优雅的防抖节流!


🎁 小贴士:别踩这些坑!

  1. this 指向丢失
    别忘了用 fn.call(this, ...args)apply 保留上下文。
  2. 立即执行 vs 延迟执行
    有些场景需要“首次立即执行,之后防抖”(比如按钮点击),可以扩展 debounce 支持 immediate 参数。
  3. 清理定时器
    在组件销毁时(如 React 的 useEffect 返回函数),记得清除定时器,避免内存泄漏。

🌈 结语:优雅,是一种生产力

防抖和节流,看似只是两个小技巧,实则是性能优化思维的缩影
不是所有事件都需要立刻响应,也不是所有请求都值得发送。

学会“延迟满足”,不仅是人生的智慧,也是前端的哲学。

下次当你看到用户疯狂敲键盘时,别慌——
你有防抖,有节流,还有闭包这位沉默的守护者。

🕊️ 让每一次请求,都恰到好处;
让每一帧渲染,都丝滑如初。