前言
在前端开发中,防抖(Debounce) 与 节流(Throttle) 是两种经典的性能优化技术,广泛应用于搜索建议、滚动加载、窗口缩放等高频事件场景。它们能有效减少不必要的函数调用,避免页面卡顿或请求爆炸。
要深入理解其实现原理,你需要掌握以下核心知识点:
闭包(Closure) :用于在函数返回后仍能"记住"并访问内部变量(如定时器 ID 或时间戳)
对于闭包,我写了这两篇文章
JavaScript 词法作用域与闭包:从底层原理到实战理解
this 与参数的正确传递:确保被包装的函数在正确上下文中运行。
对于this,有不懂的可以参考这篇文章:
本文将结合生活类比、代码实现与真实场景,带你一步步拆解防抖与节流的机制、差异与应用之道。即使你曾觉得它们"有点绕",读完也会豁然开朗。
一、问题背景:输入框频繁触发事件
全部代码在后面的附录
在 Web 开发中,用户在输入框中打字时,常会绑定 keyup 事件来实时响应输入内容。例如:
javascript
// 1.html Lines 17-19
function ajax(content) {
console.log('ajax request', content);
}
Javascript
// 1.html Lines 64-66
inputa.addEventListener('keyup', function(e) {
ajax(e.target.value); // 复杂操作
});
问题 :每当用户输入一个字符,就会触发一次 ajax() 调用。若用户输入 "hello",将产生 5 次请求,造成不必要的网络开销和性能浪费。

二、防抖(Debounce)机制
想象你站在电梯里,正等着门关上。
可就在这时,一个路人匆匆跑进来,门立刻重新打开;还没等它合拢,又一个人冲了进来......只要不断有人进入,电梯就会一直"耐心"地等下去。
我站在里面心想:"这门到底什么时候才关啊?"
直到最后,整整几秒钟没人再进来------终于,"叮"一声,门缓缓合上,电梯开始运行。
这就像防抖:只要事件还在频繁触发,函数就一直"等";只有当触发停歇了一段时间,它才真正执行。
这种"按节奏执行"的思想,不仅存在于游戏中,也广泛应用于 Web 交互。
一些AI编辑器 ( 比如Trae Cursor )就是这样
当你在代码框里飞快敲字时,它并不会每按一个键就立刻分析整段逻辑或发起智能补全请求。
那样做不仅浪费资源,还会拖慢输入体验。
相反,它会默默"观察"你的输入节奏:
只要你还在连续打字,它就耐心等待;一旦你停顿半秒,它才迅速介入,给出精准建议。
代码实现
javascript
// 1.html Lines 21-30
function debounce(fn, delay) {
let id; // 闭包中的自由变量,用于保存定时器 ID
return function(...args) {
if (id) clearTimeout(id); // 清除上一次的定时器
const that = this;
id = setTimeout(() => {
fn.apply(that, args);
}, delay);
};
}
关键点解析
防抖函数通过闭包维护一个共享的定时器标识 id,使得多次事件触发都能访问并操作同一个状态。
每当用户触发事件(如键盘输入),函数会先清除之前尚未执行的定时器(如果存在),然后重新启动一个延迟为 delay 毫秒的新定时器
这意味着只要用户持续操作,计时就会不断重置,真实逻辑始终被推迟;只有当用户停止操作并经过指定的等待时间后,目标函数才会真正执行。
以 delay = 500ms 为例,若用户在 200ms 内快速输入 "hello",每次按键都会打断之前的倒计时,最终仅在最后一次输入结束 500ms 后调用一次 ajax("hello")。整个过程将原本可能触发 5 次的请求压缩为 1 次,在保证响应合理性的同时,显著降低了系统开销。

使用示例
javascript
// 1.html Lines 58-69
const debounceAjax = debounce(ajax, 500);
inputb.addEventListener('keyup', function(e) {
debounceAjax(e.target.value);
});
三、节流(Throttle)机制
核心思想
在固定时间间隔内,最多执行一次函数。
我正在玩一款FPS游戏,手指死死按住鼠标左键疯狂扫射------
可游戏里的枪根本没跟着我的节奏"突突突"到底。明明我一秒点了十下,它却稳稳地"哒、哒、哒",每隔固定时间才射出一发子弹。
后来我才明白:这不是卡顿,而是射速限制在起作用。无论我多着急、按得多快,系统都会冷静地按自己的节奏来,既不让火力过猛破坏平衡,也不让我白白浪费弹药。
这就像节流:不管事件触发得多密集,函数都坚持"定时打卡",不多不少,稳稳执行。
这种设计哲学,同样被现代开发工具所采纳
比如京东等电商平台:鼠标滚动时,页面需要不断判断是否已滑动到商品列表底部,从而决定是否自动加载下一页商品。
如果对每一次滚动事件都立即响应,浏览器会因频繁计算和发起网络请求而卡顿,尤其在低端设备上体验更差。
于是,开发者会使用节流机制------将滚动处理函数限制为每 200~300 毫秒最多执行一次。这样,即使用户快速拖动滚动条,系统也只会在固定间隔"抽样"检查位置,既保证了加载的及时性,又避免了性能过载。
换句话说:我不在乎你滚得多快,我只按自己的节奏干活------这正是节流在真实场景中的价值。
代码实现
javascript
// 1.html Lines 32-52
function throttle(fn, delay) {
let last = 0; // 上次执行的时间戳
let deferTimer = null;
return function(...args) {
const now = Date.now();
const that = this;
if (last && now < last + delay) {
// 还未到下次执行时间:延迟执行,并确保最后一次能触发
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
fn.apply(that, args);
}, delay - (now - last));
} else {
// 可立即执行
last = now;
fn.apply(that, args);
}
};
}
关键点解析
节流函数通过闭包维护两个关键状态:
last 记录上一次实际执行的时间戳,deferTimer 则用于管理可能的延迟执行任务。
每当事件被触发,函数会先获取当前时间,并判断距离上次执行是否已超过设定的间隔 delay。
如果尚未到冷却期(即 now < last + delay),它不会立即执行,而是清除之前安排的延迟任务,并根据剩余时间重新设置一个定时器,确保在当前周期结束时至少执行一次;
如果已经过了冷却期,则直接执行函数并更新 last。这种机制既实现了"固定频率执行"的节奏控制,又巧妙地保证了在连续高频触发的末尾仍能响应最后一次操作。
例如,在 delay = 500ms 的配置下,无论用户在短时间内触发多少次事件,函数都会在 0ms、500ms、1000ms 等时间点稳定执行,既避免了过度调用,又不丢失关键的最终状态。
使用示例
javascript
// 1.html Lines 59-62
const throttleAjax = throttle(ajax, 500);
inputc.addEventListener('keyup', function(e) {
throttleAjax(e.target.value);
});
四、典型应用场景
防抖适用场景
防抖最适合那些"只关心最终结果"的交互场景。
例如,在百度或淘宝的搜索框中,用户一边输入一边期待建议词,但如果每敲一个字母就立刻发起请求,不仅会制造大量无意义的网络调用,还可能因中间态(如拼音未完成)返回错误结果。
通过防抖,系统会耐心等到用户停顿片刻(比如 300 毫秒),再以最终输入内容发起一次精准查询。
类似的逻辑也适用于表单字段的验证------只有当用户真正输完并稍作停顿,才触发校验,避免在输入过程中不断弹出错误提示干扰操作。
简言之,防抖在"太快导致资源浪费"和"太慢影响体验"之间找到了最佳平衡点。
节流适用场景
相比之下,节流则适用于需要"持续响应但必须限频"的场景。
比如在京东、掘金等电商或内容平台,用户快速滚动页面时,系统需判断是否已滑到底部以加载更多商品或帖子。若对每一次滚动都立即响应,浏览器将不堪重负。
而通过节流(如每 300 毫秒最多执行一次检查),既能及时感知滚动行为,又避免过度计算。
同样,鼠标移动或元素拖拽过程中,实时更新坐标若不加限制,极易造成界面卡顿;节流能确保 UI 以稳定帧率更新,保持流畅感。甚至在某些对 resize 事件要求实时反馈的场景(如动态调整画布或视频比例),也会采用节流而非防抖,以兼顾响应性与性能。
防抖与节流,看似简单,却是前端性能优化的基石。掌握它们,就掌握了在"响应速度"与"系统负担"之间优雅平衡的艺术。
五、完整示例代码
上面的代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖</title>
</head>
<body>
<div>
<input type="text" id="undebounce" />
<br>
<input type="text" id="debounce" />
<br>
<input type="text" id="throttle" />
</div>
<script>
function ajax(content) {
console.log('ajax request', content);
}
// 高阶函数 参数或返回值(闭包)是函数(函数就是对象)
function debounce(fn, delay) {
var id; // 自由变量
return function(args) {
if(id) clearTimeout(id);
var that = this;
id = setTimeout(function(){
fn.call(that, args)
}, delay);
}
}
// 节流 fn 执行的任务
function throttle(fn, delay) {
let
last,
deferTimer;
return function() {
let that = this; // this 丢失
let _args = arguments // 类数组对象
let now = + new Date(); // 类型转换, 毫秒数
// 上次执行过 还没到执行时间
if(last && now < last + delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function(){
last = now;
fn.apply(that, _args);
}, delay - (now - last));
} else {
last = now;
fn.apply(that, _args);
}
}
}
const inputa = document.getElementById('undebounce');
const inputb = document.getElementById('debounce');
const inputc = document.getElementById('throttle');
let debounceAjax = debounce(ajax, 500);
let throttleAjax = throttle(ajax, 500);
inputc.addEventListener('keyup', function(e) {
throttleAjax(e.target.value)
})
// 频繁触发
inputa.addEventListener('keyup', function(e) {
ajax(e.target.value) // 蛮复杂
})
inputb.addEventListener('keyup', function(e) {
debounceAjax(e.target.value)
})
</script>
</body>
</html>