本文将从核心思想→基础实现→生产级代码→应用场景→面试考点全链路讲解,所有代码均可直接复制到项目中使用。
前言
你一定遇到过这些场景:
- 输入框实时搜索,每输入一个字符就发一次请求
- 窗口 resize 时,页面疯狂重绘导致卡顿
- 滚动加载更多,滚动一次触发十次接口
- 按钮快速点击导致表单重复提交
这些问题的本质都是:短时间内高频触发的函数,造成了不必要的性能浪费。
而防抖(Debounce)和节流(Throttle),就是解决这类问题最核心、最常用的两个性能优化技术。
一、防抖(Debounce)
1. 核心思想
将短时间内多次触发的函数,合并为最后一次执行。
简单说就是:等你停下来,我再执行 。 
2. 基础版实现(理解原理)
这是最容易理解的核心逻辑,适合新手入门:
javascript
let timer = null;
input.addEventListener('keyup', function() {
// 每次触发都清除之前的定时器
if (timer) clearTimeout(timer);
// 重新开始计时
timer = setTimeout(() => {
console.log('发送搜索请求');
}, 500);
});
缺点:全局变量污染、不可复用、this 指向错误、无法传递参数。
3. 生产级实现(推荐直接使用)
解决了基础版的所有问题,支持立即执行 / 非立即执行双模式,自带取消功能:
javascript
/**
* 防抖函数
* @param {Function} fn - 需要防抖的目标函数
* @param {number} delay - 延迟时间(毫秒)
* @param {boolean} immediate - 是否立即执行(默认:false)
* @returns {Function} 防抖后的函数,自带cancel方法
*/
function debounce(fn, delay, immediate = false) {
let timer = null;
const debounced = function(...args) {
// 保存正确的this上下文
const context = this;
// 每次触发都清除之前的定时器
if (timer) clearTimeout(timer);
if (immediate) {
// 立即执行模式:只有当没有定时器时才执行
const callNow = !timer;
// 定时器仅负责重置状态,不执行函数
timer = setTimeout(() => {
timer = null;
}, delay);
// 满足条件则立即执行
if (callNow) fn.apply(context, args);
} else {
// 非立即执行模式:最后一次触发后执行
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
// 手动取消未执行的防抖
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounced;
}
// 处理函数
function handle() {
console.log(Math.random());
}
// resize事件
window.addEventListener("resize", debounce(handle, 1000));
4. 两种执行模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
immediate: false(默认) |
最后一次触发后,等待delay毫秒再执行 |
输入框搜索、自动保存 |
immediate: true |
第一次触发立即执行,之后delay毫秒内的触发都被忽略;最后一次触发后,不会再执行! |
按钮防重复点击、提交操作 |
效果如下:
我们可以看到,当持续触发resize事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发resize事件的过程中,事件处理函数handle一直没有执行。
二、节流(Throttle)
1. 核心思想
保证函数在固定时间间隔内,最多只执行一次。
简单说就是:不管你触发多少次,我每隔固定时间只执行一次 。 
2. 生产级实现(推荐直接使用)
这是目前最标准、功能最完整的节流实现,支持leading/trailing双配置:
ini
/**
* 节流函数
* @param {Function} fn - 需要节流的目标函数
* @param {number} delay - 时间间隔(毫秒)
* @param {Object} options - 配置项
* @param {boolean} options.leading - 是否在开始时执行(默认:true)
* @param {boolean} options.trailing - 是否在结束时补执行一次(默认:true)
* @returns {Function} 节流后的函数,自带cancel方法
*/
function throttle(fn, delay, options = { leading: true, trailing: true }) {
const { leading, trailing } = options;
let timer = null;
let lastTime = 0;
const throttled = function(...args) {
const context = this;
const now = Date.now();
// 处理不立即执行的情况
if (!leading && !lastTime) {
lastTime = now;
}
// 时间差达到间隔,立即执行
if (now - lastTime >= delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(context, args);
lastTime = now;
}
// 最后一次触发后,补执行一次
else if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(context, args);
lastTime = leading ? Date.now() : 0;
timer = null;
}, delay - (now - lastTime));
}
};
// 手动取消节流
throttled.cancel = function() {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}
3. 四种组合模式
| 配置 | 行为 | 适用场景 |
|---|---|---|
leading: true, trailing: true(默认) |
开始执行一次,结束再补一次 | 滚动加载更多、窗口 resize |
leading: true, trailing: false |
只在开始执行一次 | 按钮点击、鼠标移动 |
leading: false, trailing: true |
只在结束执行一次 | 拖拽元素位置更新 |
leading: false, trailing: false |
无意义,不推荐使用 | - |
三、防抖 vs 节流:一张表搞懂区别
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 核心逻辑 | 最后一次执行 | 每隔固定时间执行一次 |
| 执行次数 | 高频触发下只执行 1 次 | 高频触发下执行多次(固定频率) |
| 适用场景 | 等待用户操作结束后再执行 | 需要保证一定执行频率的场景 |
| 典型应用 | 输入框搜索、自动保存、按钮防重复点击 | 滚动加载、窗口 resize、拖拽、动画 |
一句话总结:
- 防抖:适合 "等你停下来再做" 的场景
- 节流:适合 "每隔一段时间做一次" 的场景
四、实际使用示例
1. 输入框实时搜索(防抖)
javascript
const searchInput = document.querySelector('.search-input');
function handleSearch(e) {
console.log('发送搜索请求:', e.target.value);
// 实际项目中这里调用接口
}
// 停止输入500ms后再发送请求
searchInput.addEventListener('keyup', debounce(handleSearch, 500));
2. 按钮防重复点击(防抖 - 立即执行)
javascript
const submitBtn = document.querySelector('.submit-btn');
function handleSubmit() {
console.log('提交表单');
// 实际项目中这里调用提交接口
}
// 点击后立即执行,3秒内再次点击无效
submitBtn.addEventListener('click', debounce(handleSubmit, 3000, true));
3. 滚动加载更多(节流)
javascript
function handleScroll() {
// 判断是否滚动到底部
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
console.log('加载更多数据');
// 实际项目中这里调用加载更多接口
}
}
// 每隔200ms执行一次,避免频繁触发
window.addEventListener('scroll', throttle(handleScroll, 200));
4. 框架中使用(React/Vue)
重要:组件卸载时一定要调用 cancel 方法,避免内存泄漏
javascript
// React示例
useEffect(() => {
const handleResize = throttle(() => {
console.log('窗口大小变化');
}, 200);
window.addEventListener('resize', handleResize);
// 组件卸载时取消节流
return () => {
handleResize.cancel();
};
}, []);
五、常见误区与踩坑点
1. 最经典的坑:事件绑定加括号
javascript
运行
javascript
// ❌ 错误:加了括号,函数会立即执行,不会绑定事件
window.addEventListener('resize', handle());
// ✅ 正确:不加括号,传递函数本身
window.addEventListener('resize', handle);
// ✅ 正确:防抖/节流写法也是一样
window.addEventListener('resize', debounce(handle, 500));
2. 分不清防抖和节流
- 输入框搜索:用防抖(等用户输完再搜)
- 滚动加载:用节流(每隔一段时间加载一次)
- 按钮防重复点击:用防抖(立即执行模式)
3. 组件卸载时不取消定时器
会导致内存泄漏,尤其是在单页应用中,一定要在组件卸载时调用cancel()方法。
六、高频面试题汇总
1. 什么是防抖和节流?它们有什么区别?
答:见本文第三部分。
2. 手写防抖函数
答:见本文第一部分生产级实现。
3. 手写节流函数
答:见本文第二部分生产级实现。
4. 防抖和节流的应用场景有哪些?
答:见本文第四部分。
5. 为什么防抖和节流要用闭包?
答:为了封装定时器状态(timer、lastTime),避免全局变量污染,同时保证每个防抖 / 节流函数都有自己独立的状态,互不干扰。
七、总结
防抖和节流是前端开发中最基础也最重要的性能优化技术,掌握它们不仅能解决实际开发中的性能问题,也是面试中的必考点。
本文提供的防抖和节流实现,是目前行业内最标准、最健壮的版本,可以直接复制到任何项目中使用。
最后一句话:
能用 Lodash 的
_.debounce和_.throttle就直接用,它们经过了大量生产环境的验证。但你必须理解它们的原理,这样遇到问题时才能快速排查。