“你敲得快,不代表我得跑得快。” —— 一个被频繁触发事件折磨到崩溃的 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 打印一次,稳得一批。
💎 闭包:幕后英雄
你可能注意到了,无论是防抖还是节流,都用到了闭包。
为什么?
因为我们需要在多次函数调用之间共享状态(比如 timer 或 lastTime),而不能污染全局变量。
闭包就像一个“私人保险箱”,把状态安全地藏起来,只给返回的函数使用。
🙌 没有闭包,就没有优雅的防抖节流!
🎁 小贴士:别踩这些坑!
- this 指向丢失
别忘了用fn.call(this, ...args)或apply保留上下文。 - 立即执行 vs 延迟执行
有些场景需要“首次立即执行,之后防抖”(比如按钮点击),可以扩展 debounce 支持immediate参数。 - 清理定时器
在组件销毁时(如 React 的useEffect返回函数),记得清除定时器,避免内存泄漏。
🌈 结语:优雅,是一种生产力
防抖和节流,看似只是两个小技巧,实则是性能优化思维的缩影:
不是所有事件都需要立刻响应,也不是所有请求都值得发送。
学会“延迟满足”,不仅是人生的智慧,也是前端的哲学。
下次当你看到用户疯狂敲键盘时,别慌——
你有防抖,有节流,还有闭包这位沉默的守护者。
🕊️ 让每一次请求,都恰到好处;
让每一帧渲染,都丝滑如初。