前言
作为一位AI全栈开发学习者,最近在复习高频事件的性能优化时,又把防抖和节流翻出来啃了一遍,越啃越觉得这俩东西虽然小,但用好了真的能省下不少资源,还能让交互体验更丝滑。
今天就把我的学习笔记 + 代码实践整理成一篇干货,力求讲得透彻、讲得接地气,希望能帮到正在被 input 搜索、scroll 加载、resize 这些场景折磨的你。
为什么需要防抖和节流?
先说最常见的痛点:
想象一下,你在做一个搜索框,用户每敲一个字就立刻发一次 AJAX 请求去后端拿搜索建议。

- 用户打字很快,一句话 10 个字,可能触发 10 次请求。
- 后端压力大,带宽浪费严重。
- 大部分请求其实是"无效"的,因为用户还在继续输入,前几次的结果用户根本看都不看。
再比如页面滚动加载更多:

- scroll 事件触发频率极高(浏览器一秒可能触发几十次)。
- 每次滚动都去判断是否触底、发请求加载数据,性能开销巨大,页面容易卡顿。
类似场景还有:
- window.resize
- mousemove
- 代码编辑器的自动保存或智能提示
- 按钮防止重复点击
这些事件有一个共性:触发太频繁,而我们真正关心的往往只是其中一部分触发。
这时候,防抖和节流就派上用场了,它们是前端性能优化的"降频神器"。
防抖(debounce):只关心"最后一次"
核心思想
"管你触发多少次,我只认最后一次,在你安静下来一段时间后我再执行。"
形象点说:就像你妈叫你下楼吃饭,你一直喊"等会儿",直到你连续 2 秒没喊,她才真的相信你马上就下去。
经典场景
- 搜索框输入建议(百度、淘宝搜索框)
- 表单实时校验(用户名是否可用)
- 代码编辑器智能提示
- 按钮防止短时间内重复提交
实现原理
利用定时器 + 闭包:
- 每次事件触发时,先把之前的定时器清除掉
- 重新开启一个新的定时器,delay 毫秒后执行目标函数
- 如果在 delay 时间内又触发了事件,就再清除、再重开
- 只有当连续 delay 毫秒内没有新触发,才真正执行
JavaScript
function debounce(fn, delay) {
let timer = null; // 借助闭包保存定时器 ID
return function (...args) {
// 每次触发时先把上一次的定时器干掉
if (timer) {
clearTimeout(timer);
}
// 重新开一个定时器,延迟执行
timer = setTimeout(() => {
fn.apply(this, args); // 保证 this 和参数正确传递
timer = null; // 执行完可以选择清空,方便 GC
}, delay);
};
}
立即执行版防抖(重要变种)
有时候我们希望第一次触发就立刻执行,之后在冷却时间内不重复执行(比如按钮防重点击)。
JavaScript
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args); // 第一次立即执行
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args);
}
timer = null;
}, delay);
};
}
节流(throttle):每隔一段时间就执行一次
核心思想
"不管你触发多快,我每隔固定时间只执行一次。"
就像游戏里按住自动射击的枪,射速是固定的,再怎么狂按鼠标也超不过这个频率。
经典场景
- 滚动加载更多(scroll 事件)
- 高频点击按钮做动画反馈
- mousemove 拖拽
- 游戏中的技能冷却
实现原理(时间戳版)
记录上次执行的时间戳,这次触发时判断是否已经过了 delay,如果过了就立刻执行,并更新时间戳。
JavaScript
function throttle(fn, delay) {
let last = 0; // 上次执行时间
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
}
优点:第一次触发会立即执行,最后一次如果刚好在时间窗口内也会执行。
实现原理(定时器版)
用定时器控制执行频率,适合需要"严格每 delay 毫秒执行一次"的场景。
JavaScript
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
缺点:第一次不会立即执行,最后一次触发后还要等 delay 才执行。
推荐:时间戳 + 定时器混合版(最实用)
兼顾开头立即执行 + 结尾也能执行
JavaScript
function throttle(fn, delay) {
let last = 0;
let timer = null;
return function (...args) {
const now = Date.now();
// 剩余时间
const remain = delay - (now - last);
if (remain <= 0) {
// 已经超过间隔时间
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
last = now;
} else if (!timer) {
// 在间隔内,但还没设过定时器,就补一次
timer = setTimeout(() => {
fn.apply(this, args);
last = Date.now();
timer = null;
}, remain);
}
};
}
防抖 vs 节流,一表看懂区别
| 防抖 (debounce) | 节流 (throttle) | |
|---|---|---|
| 执行时机 | 停止触发后 delay 毫秒才执行一次 | 每隔 delay 毫秒执行一次 |
| 执行次数 | 多次触发只执行最后一次 | 多次触发会执行多次(间隔执行) |
| 典型场景 | 输入搜索、表单校验 | 滚动加载、拖拽、射击游戏 |
| 比喻 | 等你说完再回消息 | 水龙头每秒只出固定水量 |
总结
- 高频事件不优化 = 性能杀手
- 防抖:适合"等用户操作结束再处理"的场景(如搜索、提交)
- 节流:适合"需要持续反馈但又不能太频繁"的场景(如滚动、拖拽)
- 两者都依赖闭包保存定时器/时间戳,是闭包最经典的应用之一
- 实际项目中优先考虑使用成熟库(如 lodash.debounce/throttle),自己手写主要是为了面试和加深理解