JavaScript 中的防抖(Debounce)和节流(Throttle)技术。它们是处理高频事件(如滚动、输入、调整窗口大小、鼠标移动等)的两种重要优化手段,能有效提升应用性能和用户体验。
核心思想与区别
| 特性 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 核心思想 | “回城被打断” | “技能冷却” |
| 形象比喻 | 电梯门:等人进完等一会儿再关门,如果中间又有人来,则重新计时。 | 游戏技能:释放一次后,必须等待冷却时间结束才能再次释放。 |
| 作用 | 将多次高频操作合并为最后一次执行。 | 确保在一定时间间隔内只执行一次操作。 |
| 适用场景 | 搜索框输入联想、窗口 resize 结束后的操作。 | 滚动加载、鼠标移动、按钮频繁点击。 |
1. 防抖 (Debounce)
防抖函数会在一连串事件触发后,等待指定的时间间隔(例如 wait 毫秒)。如果在这个等待期内没有新的事件触发,那么就执行函数。如果等待期内又有新事件触发,则取消之前的计时,并重新开始计时。
- 简单实现:
function debounce(func, wait) {
let timeoutId;
// 返回一个闭包函数,利用闭包保存定时器
return function (...args) {
// 清除上一次的定时器
clearTimeout(timeoutId);
// 设置新的定时器
timeoutId = setTimeout(() => {
func.apply(this, args); // 使用 apply 确保正确的作用域和参数
}, wait);
};
}
- 使用示例:搜索框联想
<input type="text" id="search-input" />
<script>
function fetchSuggestions(query) {
console.log(`正在搜索: ${query}`);
// 这里应该是实际的 AJAX 请求
}
const debouncedFetch = debounce(fetchSuggestions, 500);
document.getElementById('search-input').addEventListener('input', (e) => {
debouncedFetch(e.target.value); // 只有在用户停止输入500ms后才会执行
});
</script>
- 带立即执行选项的防抖 (Immediate Debounce)
有时我们希望事件第一次触发时立即执行函数,然后等到停止触发一段时间后才可以重新触发。
function debounce(func, wait, immediate) {
let timeoutId;
return function (...args) {
const callNow = immediate && !timeoutId; // 如果是立即执行模式且当前没有计时
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null; // 重置定时器ID
if (!immediate) { // 如果不是立即执行模式,则执行函数
func.apply(this, args);
}
}, wait);
if (callNow) {
func.apply(this, args);
}
};
}
2. 节流 (Throttle)
节流函数会确保一个函数在指定的时间间隔内最多只执行一次。即使事件持续频繁地触发,也会按照固定的频率执行。
实现方式主要有两种:使用定时器 或 使用时间戳。
方法一:使用定时器 (Timer)
function throttle(func, wait) {
let timeoutId;
return function (...args) {
if (!timeoutId) { // 如果定时器不存在,则可以设置
timeoutId = setTimeout(() => {
timeoutId = null; // 执行完毕后清空定时器
func.apply(this, args);
}, wait);
}
};
}
方法二:使用时间戳 (Timestamp) - 立即执行
function throttle(func, wait) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) { // 如果距离上次执行已经过了wait时间
lastTime = now;
func.apply(this, args);
}
};
}
-
两种方法的区别:
- 时间戳版:事件第一次会立即触发,停止触发后不会再额外执行一次。
- 定时器版:事件第一次会延迟
waitms 后触发,停止触发后还会再执行一次(因为最后一個定时器还会触发)。
-
结合版(常用): 兼具第一次立即执行和停止后最后执行一次的特性。
function throttle(func, wait) {
let timeoutId;
let lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = wait - (now - lastTime); // 计算还剩多久可以执行
if (remaining <= 0) {
// 如果剩余时间<=0,说明可以立即执行
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastTime = now;
func.apply(this, args);
} else if (!timeoutId) {
// 如果剩余时间>0,且没有设置定时器,则设置一个定时器在剩余时间后执行
timeoutId = setTimeout(() => {
lastTime = Date.now(); // 更新最后执行时间
timeoutId = null;
func.apply(this, args);
}, remaining);
}
};
}
- 使用示例:无限滚动加载
function checkScrollAndLoad() {
console.log('检查视口位置,加载更多数据...');
// 计算距离底部还有多少像素,如果小于阈值,则加载下一页
}
const throttledScrollHandler = throttle(checkScrollAndLoad, 200);
window.addEventListener('scroll', throttledScrollHandler);
// 即使用户疯狂滚动,每200ms也只会检查一次是否需要加载
总结与选择
| 场景 | 推荐技术 | 原因 |
|---|---|---|
| 搜索框输入联想 | 防抖 | 用户输入过程中不需要请求,只在输入停顿后请求最后一次输入的内容。 |
| 窗口大小调整 (resize) | 防抖 | 调整结束后再计算布局,避免调整过程中频繁进行昂贵的 DOM 计算。 |
| 按钮防止重复提交 | 防抖 或 节流 | 防抖(立即执行):第一次点击立即提交,之后一段时间内点击无效。节流:固定时间内只允许提交一次。 |
| 无限滚动加载 | 节流 | 在用户滚动时,按固定频率检查位置,而不是每次 scroll 事件都检查。 |
| 鼠标移动事件 (mousemove) | 节流 | 需要实时跟随但不需要极高的精度(如拖拽),用节流降低执行频率。 |
| 动画 | 通常使用 requestAnimationFrame | 这是浏览器为动画提供的专有节流API,比 setTimeout 更高效。 |
- 简单记忆:
- 防抖:关心最终状态(结果是什么)。
- 节流:关心过程状态(以固定的频率了解状态)。
在实际项目中,你可以根据具体需求选择最合适的技术,或者使用 Lodash 等库中已经高度优化过的 _.debounce 和 _.throttle 函数。


被折叠的 条评论
为什么被折叠?



