一、防抖(Debounce)VS节流 (Throttle)
(一)防抖 (Debounce)及示例
核心思想 :在事件被触发后,等待 n 秒后再执行函数。如果在这 n 秒内事件又被触发,则重新计时,等待新的 n 秒过后再执行。
通俗比喻 :回城技能
在玩MOBA游戏(如英雄联盟、王者荣耀)时,你按下回城键(触发事件),需要等待8秒才能成功回城(执行函数)。如果在8秒内你被攻击或者移动了(再次触发事件),回城会被打断,你必须重新按下回城键并再次等待8秒。
效果 :将多次频繁的操作合并为一次,只执行最后一次。
实现原理 :
使用 setTimeout 来延迟执行。每次触发事件时,都清除之前的定时器,并设置一个新的定时器。
适用场景:
-
搜索框输入建议(Search Suggest):用户连续输入时,只在用户停止输入一段时间后才向服务器发送请求,而不是每输入一个字符就请求一次。
-
窗口大小调整(Resize):等待用户调整浏览器窗口结束后,再执行计算布局的函数,避免在调整过程中频繁计算。
-
文本编辑器自动保存:在用户停止编辑一段时间后,自动保存内容。
代码示例:
javascript
function debounce(func, wait) {
let timeout;
return function() {
// 清除上一次的定时器
clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(() => {
func.apply(this, arguments);
}, wait);
};
}
// 使用示例
const myInput = document.getElementById('search');
const doAjax = () => { console.log('发送Ajax请求...'); };
myInput.addEventListener('input', debounce(doAjax, 500));
(二)节流 (Throttle)及示例
核心思想 :在一个单位时间内,无论事件被触发多少次,都只执行一次函数。
通俗比喻 :技能冷却
英雄的技能有冷却时间(CD)。你按下技能键(触发事件),技能会立即释放(执行函数),然后进入CD。在CD期间,无论你按多少次技能键,技能都不会再次释放,直到CD结束。
效果 :控制函数执行的频率,使其以一个固定的、较低的频率执行。
实现原理 :
可以通过时间戳或定时器来实现。常见的是使用一个标志位(或者时间差)来判断是否应该执行函数。
适用场景:
-
滚动加载(Infinite Scroll) :在用户滚动页面时,每隔一定时间检查一次滚动位置,看是否接近底部,而不是在每次
scroll事件都检查。 -
鼠标移动(Mousemove):比如实现一个拖拽功能,不需要获取每一个像素的移动坐标,只需每隔一段时间获取一次即可。
-
按钮点击(防止重复提交):防止用户在短时间内多次点击提交按钮。
代码示例(使用时间戳版本):
javascript
function throttle(func, wait) {
let lastTime = 0;
return function() {
const now = Date.now();
// 如果距离上次执行的时间超过了等待时间,则执行函数
if (now - lastTime >= wait) {
func.apply(this, arguments);
lastTime = now; // 更新最后一次执行的时间
}
};
}
// 使用示例
const myButton = document.getElementById('submit');
const handleClick = () => { console.log('提交表单...'); };
myButton.addEventListener('click', throttle(handleClick, 2000)); // 2秒内最多执行一次
(三)核心区别总结(表格对比)
| 特性 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 核心思想 | 延迟执行,只执行最后一次 | 间隔执行,固定频率执行 |
| 执行时机 | 在连续触发结束后,等待一段时间执行 | 在连续触发过程中,按照固定时间间隔执行 |
| 是否重置 | 每次新触发都会重置计时器 | 新触发不会重置计时,按固定节奏执行 |
| 类比 | 回城技能(不停打断,只回一次) | 技能冷却(CD好了就放,放完进入CD) |
| 适用场景 | 搜索建议、Resize | 滚动加载、拖拽、频繁点击 |
如何选择?
-
当你关心最终状态 时,使用防抖。例如,搜索框只关心用户最后输入了什么。
-
当你需要在过程中以可控的节奏处理事件 时,使用节流。例如,滚动时你需要持续地判断是否到了页面底部。
++简单来说:防抖是"最后一个人说了算",节流是"按规矩办事,一段时间内只办一次"。++
(四)防抖:节流使用≈7:3

为什么感觉"节流不怎么用"?
您的观察是正确的,在现代前端开发中,防抖的使用场景确实比节流更加普遍和直观。主要原因如下:
1. UI/UX 层面的替代方案更加优雅
就像您提到的按钮防重复点击的场景:
-
节流方案:用节流函数包装点击处理函数,设置2秒内只能点击一次
-
更优方案 :点击后立即显示 loading 状态(禁用按钮+加载动画)
-
用户体验更好(明确知道状态)
-
代码更直观易维护
-
能更好地处理异步操作的成功/失败状态
-
javascript
// 节流方案(不够优雅)
const handleSubmit = throttle(() => {
// 提交逻辑
}, 2000);
// 更优方案(推荐)
const handleSubmit = async () => {
setLoading(true);
button.disabled = true;
try {
await submitForm();
// 成功处理
} catch (error) {
// 错误处理
} finally {
setLoading(false);
button.disabled = false;
}
};
2. 防抖有不可替代的独特场景
输入框搜索建议这个场景,防抖几乎是唯一的最佳选择:
-
用户连续输入时,我们不关心中间过程,只关心最终输入的内容
-
如果使用节流,仍然会在输入过程中频繁请求,达不到优化目的
-
防抖能完美地"等待用户完成输入"
javascript
// 防抖在输入框中的应用(无可替代)
const searchInput = document.getElementById('search');
const fetchSuggestions = debounce(async (query) => {
const results = await api.getSuggestions(query);
renderSuggestions(results);
}, 300);
searchInput.addEventListener('input', (e) => {
fetchSuggestions(e.target.value);
});
3. 节流的传统场景被新技术替代
一些传统的节流使用场景现在有了更好的解决方案:
-
滚动加载 :现在更多使用 Intersection Observer API,性能更好且更精确
-
Resize 监听:同样可以用 Resize Observer API 替代
但节流仍有其价值场景
虽然使用频率不如防抖,但节流在以下场景中仍然很重要:
1. 实时性要求较高的交互
javascript
// 拖拽元素 - 需要实时反馈,但不能过于频繁
const dragElement = document.getElementById('draggable');
const updatePosition = throttle((x, y) => {
element.style.left = x + 'px';
element.style.top = y + 'px';
}, 16); // ~60fps
dragElement.addEventListener('mousemove', (e) => {
updatePosition(e.clientX, e.clientY);
});
2. 频繁的事件监听
javascript
// 窗口滚动时记录位置 - 用于数据分析
const trackScroll = throttle(() => {
analytics.track('scroll_position', window.scrollY);
}, 1000); // 每秒最多记录一次
window.addEventListener('scroll', trackScroll);
3. 游戏或动画相关
在游戏开发中,节流常用于限制某些操作的频率,比如技能冷却、射击间隔等。
二、防抖实现-鸿蒙

类实现
TypeScript
class Debouncer {
private delay: number = 300
private timer: number = 0
constructor(delay: number = 300) {
this.delay = delay;
}
debounce(func: Function) {
// 返回一个新的函数
return (...args: ESObject[]) => {
// 清除之前的定时器
if (this.timer) {
clearTimeout(this.timer);
}
// 设置新的定时器
this.timer = setTimeout(() => {
// 使用闭包访问 func 和 args
func(...args);
}, this.delay);
};
}
}
export {
Debouncer
}
// 使用
const debouncer: Debouncer = new Debouncer(300); // 防抖函数
Row() {
ForEach(this.downMenuList, (item: ItemModel2) => {
Row() {
Text(item.label)
.fontSize(16)
.fontColor('#333')
Image($r('app.media.icon_arrow_b'))
.width(10)
.rotate({
angle: this.iconRotate[item.value - 1]
})
.margin({ left: 5 })
}
.width('50%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(debouncer.debounce(() => {
this.handleDownMenu(item)
}))
})
//
闭包版本的防抖实现
TypeScript
function createDebouncer(delay: number = 300) {
let timer: number = 0; // 闭包变量 - 替代 this.timer
// 返回防抖函数
return (func: Function) => {
// 返回实际执行的函数
return (...args: any[]) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func(...args);
}, delay); // 使用闭包中的 delay
};
};
}
// 使用方式
const debouncer = createDebouncer(500);
const debouncedFunc = debouncer((plate: string) => {
console.log('校验车牌:', plate);
});
// 调用
debouncedFunc('粤B12345');
闭包:
