防抖与节流的性能优化 🚀
作为前端开发者,你是否遇到过这样的场景:用户疯狂敲击键盘搜索时,控制台像放烟花一样刷满了请求日志;或者滚动页面时,浏览器卡得像幻灯片?其实这些问题都能通过 "防抖" 和 "节流" 这两个小技巧解决,而它们的实现背后,都藏着闭包的身影。今天咱们就来扒一扒这对性能优化界的 "黄金搭档"~
第一部分:防抖 ------"等你停了我再动" ⏸️
1. 情景引入:被代码提示打断思路的痛 😣
有没有遇到过在编辑器里疯狂敲代码?比如写const user = { name: '时,编辑器如果每敲一个字符就弹一次代码提示框,那画面简直太美不敢看 ------ 光标被遮挡、思路被打断,分分钟想砸键盘!
但聪明的编辑器不会这么干,它会等你停顿半秒后,再慢悠悠地弹出精准提示。这种 "等你停了再行动" 的逻辑,就是防抖的精髓。
2. 无防抖的 "灾难现场":keyup 事件的疯狂输出 💥
先来看个反面教材。假设我们有个搜索框,想在用户输入时实时发送请求获取建议,代码可能长这样:
html
<!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" />
<script>
// 模拟AJAX请求的函数
function ajax(content) {
console.log('ajax request', content);
}
// 获取输入框元素
const inputa = document.getElementById('undebounce');
// 绑定keyup事件:每次按键抬起就触发请求
inputa.addEventListener('keyup', function (e) {
// 把输入框的值传给ajax函数
ajax(e.target.value);
});
</script>
</div>
</body>
</html>
代码详解:
- 给输入框绑定了
keyup事件,意思是 "用户每松开一个按键,就执行一次回调" - 回调里直接调用
ajax函数,把当前输入的内容传过去 - 看起来很简单,但实际运行起来...
代码效果:
当你输入 "hello" 这 5 个字母时,控制台会疯狂输出 5 条请求日志:
ajax request h
ajax request he
ajax request hel
ajax request hell
ajax request hello

如果用户打字快,一秒能敲 10 个键,那一秒就会发 10 次请求 ------ 服务器表示:"我承受了太多不该承受的压力 😭"
思考:
这显然不对劲!用户真正需要的是 "输入完一段内容后" 的一次请求,而不是 "输入过程中" 的 N 次无效请求。就像你打电话给外卖小哥,不会每说一个字就挂了重打,而是说完一整句话再等回复,对不?
所以我们的需求很明确:管你keyup触发多少次,我只执行最后一次------ 这就是防抖要干的事!
3. 防抖的实现:给事件加个 "冷静期" 🧠
具体实现逻辑:
防抖的核心是 "延迟执行 + 清空重计",需要两个帮手:
- 定时器(setTimeout) :让函数延迟执行,给用户留出 "冷静期"
- 闭包:保存定时器 ID,方便每次触发时清除上一个定时器
步骤拆解:
- 第一次触发事件时,设置一个定时器,计划延迟
delay时间后执行目标函数 - 如果在
delay时间内再次触发事件,就清除上一个定时器,重新设置一个新的 - 直到用户停止触发事件,且超过
delay时间,定时器才会真正执行目标函数
4.代码实现:
javascript
// 防抖函数:接收目标函数fn和延迟时间delay
function debounce(fn, delay) {
var id; // 闭包保存的定时器ID(自由变量,不会被垃圾回收)
// 返回包装后的函数,接收事件参数
return function (args) {
// 如果已有定时器,先清除(上一次的触发作废)
if (id) {
clearTimeout(id);
}
var that = this; // 保存当前this指向(比如输入框元素)
// 重新设置定时器,延迟执行fn
id = setTimeout(function () {
// 用call保证fn的this指向正确,同时传递参数
fn.call(that, args);
}, delay);
}
}
// 使用防抖包装ajax函数,延迟1000毫秒(1秒)
let debounceAjax = debounce(ajax, 1000);
// 给防抖输入框绑定事件
inputb.addEventListener('keyup', function (e) {
debounceAjax(e.target.value);
});
代码详解:
这里的debounce是个高阶函数(参数和返回值都是函数),它利用闭包把id(定时器 ID)藏了起来。就像你给快递柜设了个取件码,只有你知道这个码,每次取件前可以先取消上一次的预约(清除定时器),重新设置新的取件时间。
生活例子:电梯关门 🛗电梯里没人按按钮时,门会倒计时 3 秒后关闭;但如果这 3 秒内有人按了开门键,电梯会重新倒计时 3 秒 ------ 直到最后 3 秒内没人操作,门才会真正关上。这里的 "3 秒" 就是delay,"重新倒计时" 就是清除定时器后重设,"最终关门" 就是执行目标函数。
5. 防抖的优点:
- 减少无效请求:从 "每键一次发一次" 变成 "最后一次停顿后发一次",服务器压力大减
- 提升用户体验:避免频繁更新 UI 导致的闪烁(比如搜索建议框疯狂抖动)
- 资源优化:节省带宽和浏览器性能,尤其适合复杂的计算或请求
第二部分:节流 ------"到点了我再动" ⏳
1. 情景引入:滚动加载的 "刹车难题" 🛑
刷朋友圈时,你有没有想过:为什么快速滚动屏幕,不会每滚 1 像素就加载一次新内容?如果真这样,手机早就卡成 PPT 了。
实际上,滚动加载会设置一个 "冷却时间",比如 500 毫秒 ------ 不管你在这 500 毫秒内滚得多疯狂,它最多只加载一次新内容。这种 "按固定节奏执行" 的逻辑,就是节流。
2. 思考:节流能解决 keyup 的频繁触发吗?
当然能!如果说防抖是 "等你停了再执行",那节流就是 "不管你停没停,到点了就执行一次"。就像上课铃每 45 分钟响一次,不管你中间举手多少次,下课时间到了才会响 ------ 这就是节流的脾气~
3. 节流的实现:给事件加个 "节拍器" 🎵
具体实现逻辑:
节流的核心是 "固定间隔执行",需要两个闭包变量:
last:上一次执行目标函数的时间戳deferTimer:延迟执行的定时器 ID(处理最后一次触发)
步骤拆解:
-
第一次触发事件时,直接执行目标函数,记录当前时间为
last -
后续触发时,计算当前时间与
last的差值:- 如果差值 < 间隔时间
delay:设置定时器,等delay时间后执行一次(防止最后一次触发被漏掉) - 如果差值 >= 间隔时间
delay:直接执行目标函数,更新last为当前时间
- 如果差值 < 间隔时间
4.代码实现:
javascript
// 节流函数:接收目标函数fn和间隔时间delay
function throttle(fn, delay) {
let last, deferTimer; // 闭包保存:上一次执行时间、延迟定时器ID
return function (args) {
let that = this; // 保存当前this指向
let _args = arguments; // 保存参数(兼容多参数场景)
let now = +Date.now(); // 当前时间戳(+号快速转数字)
// 如果已有上一次执行时间,且当前在间隔时间内
if (last && now < last + delay) {
// 清除之前的延迟定时器
clearTimeout(deferTimer);
// 设置新的延迟定时器,确保最后一次触发能执行
deferTimer = setTimeout(function () {
last = now; // 更新上一次执行时间
fn.apply(that, _args); // 执行目标函数
}, delay);
} else {
// 首次触发或已过间隔时间,直接执行
last = now; // 更新上一次执行时间
fn.apply(that, _args); // 执行目标函数
}
}
}
// 使用节流包装ajax函数,间隔1000毫秒(1秒)
let throttleAjax = throttle(ajax, 1000);
// 给节流输入框绑定事件
inputc.addEventListener('keyup', function (e) {
throttleAjax(e.target.value);
});
代码详解:
throttle函数同样利用闭包保存了last和deferTimer。它就像个严格的监考老师,规定 "每 1 小时只能交一次卷"------ 就算你提前写完,也得等够 1 小时才能交;但如果到了 1 小时还没写完,也必须交卷(用定时器保证最后一次执行)。
生活例子:机枪射速 🔫很多游戏里的机枪有 "射速限制",比如每秒最多射 3 发。不管你把鼠标按得多快,1 秒内只会射出 3 发子弹,不会因为按得快就无限连发 ------ 这里的 "每秒 3 发" 就是节流的间隔逻辑。
5. 节流的优点:
- 控制执行频率:避免函数在高频触发时 "扎堆执行",保证浏览器和服务器的稳定
- 平衡响应性:不会像防抖那样完全等待停顿,而是按固定节奏响应,适合需要持续反馈的场景
- 资源分配合理:把密集的任务 "平均分配" 到时间轴上,避免瞬间资源占用过高
第三部分:完整代码与效果展示 🎬
代码亮个相:
为了直观对比,咱们把 "无处理、防抖、节流" 三种情况放一起:
html
<!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" placeholder="无防抖节流(实时触发)" />
<br>
<!-- 防抖:输入停止后延迟触发请求 -->
<input type="text" id="debounce" placeholder="防抖(停止输入1秒后触发)" />
<br>
<!-- 节流:固定时间间隔内仅触发一次请求 -->
<input type="text" id="throttle" placeholder="节流(每1秒最多触发一次)" />
</div>
<script>
/**
* 模拟AJAX请求函数
* @param {string} content - 输入框的内容,作为请求参数
*/
function ajax(content) {
// 打印模拟请求,实际场景中这里是真实的接口调用(如fetch、axios)
console.log('ajax request', content);
}
/**
* 防抖函数(debounce)
* 核心逻辑:高频触发时,只保留最后一次触发,且需等待延迟时间结束后才执行
* 特性:触发后不会立即执行,停止触发后延迟执行,适合搜索输入、窗口resize等场景
* @param {Function} fn - 需要防抖处理的目标函数(如ajax)
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} - 包装后的防抖函数
*/
// 高阶函数:参数是函数(fn),返回值也是函数,利用闭包保存自由变量(id)
function debounce(fn, delay) {
var id; // 自由变量:保存定时器ID,用于后续清除定时器(闭包特性,不会被垃圾回收)
// 返回包装后的防抖函数,接收目标函数(fn)的参数
return function (args) {
// 如果已有定时器(说明当前处于高频触发中),先清除上一个定时器
if (id) {
clearTimeout(id);
}
var that = this; // 保存当前this指向(确保目标函数fn的this指向正确,如绑定到输入框)
// 重新设置定时器,延迟执行目标函数
id = setTimeout(function () {
// 用call改变fn的this指向为that,同时传递参数args
// 确保fn执行时的this和参数与直接调用一致
fn.call(that, args);
}, delay);
}
}
/**
* 节流函数(throttle)
* 核心逻辑:高频触发时,限制函数在固定时间间隔内只能执行一次
* 特性:触发后会优先执行一次(首次),后续高频触发时按固定间隔执行,适合滚动加载、按钮防连点等场景
* @param {Function} fn - 需要节流处理的目标函数(如ajax)
* @param {number} delay - 时间间隔(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(fn, delay) {
let last, deferTimer; // 自由变量(闭包保存)
// last:上一次执行目标函数的时间戳
// deferTimer:延迟执行的定时器ID,用于处理最后一次触发的遗留任务
return function (args) {
let that = this; // 保存当前this指向,确保fn的this指向正确
let _args = arguments; // 保存参数列表(类数组对象),兼容多参数场景
let now = +Date.now(); // 类型转换:将当前时间对象转为毫秒级时间戳(+号简化转换)
// 条件判断:如果存在上一次执行时间,且当前时间未超过「上一次执行时间+间隔时间」
// 说明当前处于时间间隔内,暂不允许直接执行
if (last && now < last + delay) {
// 清除之前的延迟定时器,避免重复执行
clearTimeout(deferTimer);
// 设置新的延迟定时器,确保最后一次触发在间隔时间结束后能执行
deferTimer = setTimeout(function () {
last = now; // 更新上一次执行时间为当前时间
// 用apply改变fn的this指向,传递参数列表(apply适合类数组参数)
fn.apply(that, _args);
}, delay);
} else {
// 首次触发 或 当前时间已超过「上一次执行时间+间隔时间」,直接执行目标函数
last = now; // 更新上一次执行时间为当前时间
fn.apply(that, _args); // 立即执行目标函数
}
}
}
// 1. 获取三个输入框DOM元素
const inputa = document.getElementById('undebounce'); // 无防抖节流的输入框
const inputb = document.getElementById('debounce'); // 防抖处理的输入框
const inputc = document.getElementById('throttle'); // 节流处理的输入框
// 2. 生成防抖和节流处理后的ajax函数(延迟/间隔时间均为1000毫秒=1秒)
let debounceAjax = debounce(ajax, 1000);
let throttleAjax = throttle(ajax, 1000);
// 3. 为输入框绑定keyup事件(输入内容时触发)
// 无防抖节流:输入时每按一个键就触发一次ajax,高频触发
inputa.addEventListener('keyup', function (e) {
ajax(e.target.value); // 直接调用ajax,e.target.value是输入框当前内容
});
// 防抖处理:输入时不触发,停止输入1秒后才触发一次ajax
inputb.addEventListener('keyup', function (e) {
debounceAjax(e.target.value); // 调用包装后的防抖函数
});
// 节流处理:输入时每1秒最多触发一次ajax,即使高频输入也会限制执行频率
inputc.addEventListener('keyup', function (e) {
throttleAjax(e.target.value); // 调用包装后的节流函数
});
</script>
</body>
</html>
效果亮个相:
当我在输入框输入"forntent"时,观察控制台效果:
- 无处理输入框:控制台刷出 8 条日志(f/r/o/n/t/e/n/t 各一次)
- 防抖输入框:停顿 1 秒后,只刷出 1 条 "frontent"
- 节流输入框:每 1 秒刷一次,假设输入用了 3 秒,会刷出 "f"、"fronte" 和 "frontent" 3条

第四部分:防抖和节流的对比 🔍
1. 核心逻辑:"留最后" vs "控频率"(最本质区别)
- 防抖:核心是「等待最后一刻」,它不关心触发过程,只认在一定时间内的 "最后一次触发",所有中间触发都会被清除丢弃,只有当触发停止且延迟时间耗尽,才会执行最终的逻辑。比如:你给客服发消息,连续发了 5 条,防抖就相当于等你停发 10 秒后,一次性把最后 1 条消息发给客服,前 4 条全部丢弃。
- 节流:核心是「匀速响应」,它不丢弃过多关键触发,而是按照固定时间间隔 "筛选" 触发,每到间隔时间就执行一次,即使触发未停止,也会平稳输出结果。比如:同样连续发 5 条消息,节流就相当于每 10 秒发 1 条,不管你发得多快,都按 10 秒间隔依次发送,既不会刷屏,也不会遗漏核心信息。
2. 执行时机:"停了才执行" vs "到点就执行"
- 防抖:是「被动执行」,必须等高频触发停止后,才能执行,触发过程中完全无响应,适合不需要实时反馈的场景。
- 节流:是「主动执行」,只要触发开始,到了固定时间间隔就会执行,触发过程中持续有反馈,适合需要一定实时性的场景。
3. 执行次数:"一次到位" vs "分批执行"
- 防抖:高频触发期间,执行次数固定为「1 次」,仅对应最后一次触发的结果,没有中间过程的执行。
- 节流:高频触发期间,执行次数「大于等于 1 次」,触发时长越长、时间间隔越短,执行次数越多,是分批、匀速的执行模式。
| 维度 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 核心逻辑 | 一定时间内只执行最后一次 | 每隔一定时间执行一次 |
| 执行时机 | 停止触发后延迟执行 | 触发后立即执行(首次)+ 固定间隔执行 |
| 生活例子 | 电梯关门(等人上完再关) | 机枪射速(固定时间射一发) |
| 应用场景 | 搜索建议、窗口 resize、按钮防重复提交 | 滚动加载、高频点击(如射击游戏)、视频弹幕 |
| 闭包作用 | 保存定时器 ID,用于清除上一次延迟 | 保存上次执行时间和延迟定时器,控制间隔 |
第五部分:面试官会问这些哦 🤔
-
防抖和节流的区别是什么? 答:防抖是 "等停止触发后执行最后一次",节流是 "按固定间隔执行"。比如搜索输入用防抖(等输完再查),滚动加载用节流(按节奏加载)。
-
它们为什么要用闭包实现? 答:因为需要保存 "定时器 ID" 或 "上次执行时间" 这些状态,闭包能让这些变量在函数调用间持续存在,不会被垃圾回收。
-
什么场景用防抖,什么场景用节流? 答:需要 "等待用户操作结束" 的场景用防抖(如输入搜索);需要 "持续反馈但控制频率" 的场景用节流(如滚动、拖拽)。
第六部分:结语 🎯
防抖和节流就像前端性能优化的 "双保险"------ 它们用闭包的特性巧妙控制函数执行时机,既避免了资源浪费,又提升了用户体验。
记住:防抖是 "等你停",节流是 "按时来" 。下次遇到高频触发的事件(keyup、scroll、resize...),先想想该请哪位 "大神" 出山吧~🏆