为什么需要防抖与节流?
在 Web 开发中,我们经常会遇到一些高频触发的事件,比如:
- 窗口滚动(scroll):用户滚动页面时,事件每秒可能触发数十次甚至上百次。
- 窗口大小调整(resize):拖动浏览器窗口时,事件持续高频触发。
- 输入框输入(input):用户每敲一个字符都会触发一次。
- 鼠标移动(mousemove):鼠标在页面上移动时,事件触发频率极高。
如果每次事件触发都执行复杂的逻辑(比如发送网络请求、操作 DOM、进行大量计算),页面性能会急剧下降,甚至出现卡顿、掉帧等体验问题。
防抖(Debounce) 和 节流(Throttle) 就是解决这类问题的两种核心优化手段。
一、防抖(Debounce)
1.1 原理
防抖的核心思想是:在事件被触发后,延迟 n 秒再执行回调。如果在这 n 秒内事件又被触发,则重新计时。
简单来说,就是“等你停下来我再执行”。就像电梯门:只要还有人进来,电梯就不会关门,直到最后一个人进来后等待几秒,门才会关上。
1.2 适用场景
- 搜索框输入联想:用户停止输入后再发送请求
- 表单验证:用户停止输入后再校验
- 按钮防连点:防止用户短时间内多次点击提交
- 窗口 resize:调整结束后再重新计算布局
1.3 手写实现
基础版本:
function debounce(fn, delay) {
let timer = null;
return function (...args) {
// 每次触发都清除上一个定时器,重新计时
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
带立即执行选项的版本:
有时候我们希望第一次触发时立即执行,之后的触发才进行防抖(比如按钮点击,希望第一次点击立刻响应):
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate) {
// 如果还没有定时器,说明是第一次触发,立即执行
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (callNow) {
fn.apply(this, args);
}
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};
}
带取消功能的版本:
在实际项目中,我们可能需要在组件卸载时取消未执行的防抖操作:
function debounce(fn, delay, immediate = false) {
let timer = null;
function debounced(...args) {
if (timer) clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (callNow) {
fn.apply(this, args);
}
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
}
// 添加取消方法
debounced.cancel = function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
二、节流(Throttle)
2.1 原理
节流的核心思想是:在单位时间内,事件回调最多执行一次。 无论事件触发多少次,都保证在指定的时间间隔内只执行一次。
简单来说,就是“我不管你触发多快,我按固定频率执行”。就像水龙头滴水,无论水管里水流多急,滴水的频率是固定的。
2.2 适用场景
- 滚动加载更多:每隔一段时间检测一次滚动位置
- 拖拽移动:按固定频率更新位置
- 游戏中的射击:限制射击频率
- 高频点击提交:限制提交频率
2.3 手写实现
时间戳版本(首次立即执行):
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
// 距离上次执行超过了 interval 才执行
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
定时器版本(首次延迟执行):
function throttle(fn, interval) {
let timer = null;
return function (...args) {
if (timer) return; // 还在冷却中,直接忽略
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
};
}
时间戳 + 定时器结合版(首次立即执行 + 最后一次也执行):
这是最完善的版本,既保证首次立即执行,又保证事件停止触发后还能执行最后一次:
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
// 如果距离上次执行已超过 interval,立即执行
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
} else if (!timer) {
// 否则设置定时器,保证最后一次也能执行
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
三、防抖 vs 节流:核心区别
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 事件停止触发后延迟执行 | 固定时间间隔内执行一次 |
| 触发频率 | 不固定,取决于用户停止操作的时间 | 固定,按设定间隔执行 |
| 类比 | 电梯门(等人进完再关) | 红绿灯(固定时间切换) |
| 适用场景 | 搜索联想、表单验证 | 滚动加载、拖拽、高频点击 |
一句话总结:防抖是“等你不动了我再动”,节流是“你动你的,我按我的节奏动”。
四、实际应用示例
搜索框防抖
const searchInput = document.getElementById('search');
function fetchSuggestions(keyword) {
console.log('发送请求,搜索:', keyword);
// 实际项目中这里会发送 AJAX 请求
}
const debouncedSearch = debounce(fetchSuggestions, 500);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
滚动节流
function handleScroll() {
console.log('当前滚动位置:', window.scrollY);
// 实际项目中这里可能做懒加载、回到顶部按钮显示等
}
const throttledScroll = throttle(handleScroll, 200);
window.addEventListener('scroll', throttledScroll);
五、使用注意事项
- this 指向问题:手写实现中必须用
fn.apply(this, args)而非直接fn(args),否则会丢失调用者的this上下文。 - 参数传递:使用剩余参数
...args确保原函数的参数能正确传递。 - 取消功能:在组件化开发中(如 React、Vue),组件卸载时需要取消未执行的定时器,避免内存泄漏。
- 选择哪种:需要“最后一次”生效用防抖,需要“固定频率”生效用节流。实际开发中也可以结合使用。
- lodash 的替代:如果不想手写,可以直接使用
lodash.debounce和lodash.throttle,它们提供了更完善的配置项和边界处理。
总结
防抖和节流是前端性能优化中最基础也最重要的两个工具。理解它们的核心原理——防抖是“延迟执行 + 重新计时”,节流是“固定频率执行”——能够帮助我们在面对不同场景时做出正确的选择。手写实现不仅能加深理解,还能在面试中展现扎实的 JavaScript 基础功底。
在实际项目中,建议优先使用成熟的工具库(如 lodash),同时在理解原理的基础上,根据具体业务需求进行定制化实现。
未经允许不得转载:任鹏个人博客 » 防抖与节流:高频事件性能优化的原理与手写实现

