JavaScript 防抖与节流进阶:从原理到实战
当用户疯狂点击按钮、疯狂滚动页面、疯狂输入搜索关键词时,应用还能流畅运行吗?防抖(Debounce)和节流(Throttle)是应对高频事件的终极武器。本文将从源码层面深入理解它们的差异,并实现一个支持立即执行、延迟执行、取消功能、记录参数的完整版本。
前言:高频事件带来的挑战
我们先来看一些简单的场景:
window.addEventListener('resize', () => {
// 窗口大小改变时重新计算布局
recalcLayout(); // 一秒可能触发几十次!
});
searchInput.addEventListener('input', () => {
// 用户每输入一个字符就发起搜索请求
fetchSearchResults(input.value); // 浪费大量请求!
});
window.addEventListener('scroll', () => {
// 滚动时加载更多数据
loadMoreData(); // 滚动一下触发几十次!
});
在这些场景中,当事件触发频率远高于我们需要的处理频率,就会出现卡顿、闪屏等现象,这就是防抖和节流要解决的核心问题。
理解防抖与节流的本质差异
核心概念对比
| 类型 | 防抖 | 节流 |
|---|---|---|
| 概念 | 将多次高频操作合并为一次,仅在最后一次操作后的延迟时间到达时执行 | 保证在单位时间内只执行一次,稀释执行频率 |
| 场景示例 | 电梯关门:等最后一个人进来后才关门,中间如果有人进来就重新计时 | 地铁安检:无论多少人排队,每秒钟只能通过一个人 |
| 执行次数 | 只执行最后一次 | 定期执行,不保证最后一次 |
| 频率 | N次高频调用 → 1次执行 | N次高频调用 → N/间隔时间次执行 |
适用场景对比
防抖场景
- 搜索框输入(用户停止输入后才搜索)
- 窗口大小调整(窗口调整完成后重新计算)
- 表单验证(用户输完才验证)
- 自动保存(停止编辑后保存)
- 按钮防连点(避免重复提交)
节流场景
- 滚动加载更多(滚动过程中定期检查)
- 动画帧(控制动画执行频率)
- 游戏循环(固定帧率)
- 鼠标移动事件(实时位置但不过度频繁)
- DOM元素拖拽(平滑移动)
防抖函数实现
基础防抖实现
function debounce(fn, delay) {
let timer = null;
return function (...args) {
// 每次调用都清除之前的定时器
if (timer) {
clearTimeout(timer);
}
// 设置新的定时器
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
支持立即执行的防抖
function debounceEnhanced(fn, delay, immediate = false) {
let timer = null;
let lastContext = null;
let lastArgs = null;
let lastResult = null;
let callCount = 0;
return function (...args) {
lastContext = this;
lastArgs = args;
// 第一次调用且需要立即执行
if (immediate && !timer) {
lastResult = fn.apply(lastContext, lastArgs);
callCount++;
console.log(`立即执行 (调用 #${callCount})`);
}
// 清除之前的定时器
if (timer) {
clearTimeout(timer);
}
// 设置延迟执行
timer = setTimeout(() => {
// 如果不是立即执行模式,或者已经执行过立即执行
if (!immediate) {
lastResult = fn.apply(lastContext, lastArgs);
callCount++;
console.log(`延迟执行 (调用 #${callCount})`);
}
// 清理
timer = null;
lastContext = null;
lastArgs = null;
}, delay);
return lastResult;
};
}
完整版防抖(支持取消、取消、参数记录)
class DebouncedFunction {
constructor(fn, delay, options = {}) {
this.fn = fn;
this.delay = delay;
this.immediate = options.immediate || false;
this.maxWait = options.maxWait || null;
this.timer = null;
this.lastArgs = null;
this.lastContext = null;
this.lastResult = null;
this.lastCallTime = null;
this.lastInvokeTime = null;
// 参数历史记录
this.history = [];
this.maxHistory = options.maxHistory || 10;
// 调用次数统计
this.stats = {
callCount: 0,
invokedCount: 0,
canceledCount: 0
};
}
/**
* 执行函数
*/
_invoke() {
const time = Date.now();
this.stats.invokedCount++;
this.lastInvokeTime = time;
// 记录参数历史
if (this.lastArgs) {
this.history.push({
args: [...this.lastArgs],
timestamp: time,
type: this.timer ? 'delayed' : 'immediate'
});
// 限制历史记录数量
if (this.history.length > this.maxHistory) {
this.history.shift();
}
}
// 执行原函数
this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);
// 清理
this.lastArgs = null;
this.lastContext = null;
return this.lastResult;
}
/**
* 调用防抖函数
*/
call(...args) {
const now = Date.now();
this.stats.callCount++;
this.lastArgs = args;
this.lastContext = this;
this.lastCallTime = now;
// 立即执行模式处理
if (this.immediate && !this.timer) {
this._invoke();
}
// 清除现有定时器
if (this.timer) {
clearTimeout(this.timer);
}
// 最大等待时间处理
if (this.maxWait && this.lastInvokeTime) {
const timeSinceLastInvoke = now - this.lastInvokeTime;
if (timeSinceLastInvoke >= this.maxWait) {
this._invoke();
return this.lastResult;
}
}
// 设置新的定时器
this.timer = setTimeout(() => {
// 非立即执行模式,或者已经执行过立即执行
if (!this.immediate) {
this._invoke();
}
this.timer = null;
}, this.delay);
return this.lastResult;
}
/**
* 取消当前待执行的防抖
*/
cancel() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
this.stats.canceledCount++;
}
this.lastArgs = null;
this.lastContext = null;
}
/**
* 立即执行并取消后续
*/
flush() {
if (this.lastArgs) {
this._invoke();
this.cancel();
}
return this.lastResult;
}
/**
* 判断是否有待执行的任务
*/
pending() {
return this.timer !== null;
}
/**
* 获取调用历史
*/
getHistory() {
return [...this.history];
}
/**
* 获取统计信息
*/
getStats() {
return { ...this.stats };
}
/**
* 重置状态
*/
reset() {
this.cancel();
this.history = [];
this.stats = {
callCount: 0,
invokedCount: 0,
canceledCount: 0
};
this.lastResult = null;
this.lastCallTime = null;
this.lastInvokeTime = null;
}
}
节流函数实现
基础节流实现
function throttleTimer(fn, interval) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
}
};
}
完整版节流(支持首尾执行)
class ThrottledFunction {
constructor(fn, interval, options = {}) {
this.fn = fn;
this.interval = interval;
this.leading = options.leading !== false; // 是否立即执行
this.trailing = options.trailing !== false; // 是否最后执行
this.timer = null;
this.lastArgs = null;
this.lastContext = null;
this.lastResult = null;
this.lastInvokeTime = 0;
// 参数历史
this.history = [];
this.maxHistory = options.maxHistory || 10;
// 统计信息
this.stats = {
callCount: 0,
invokedCount: 0,
throttledCount: 0
};
}
/**
* 执行函数
*/
_invoke() {
const now = Date.now();
this.lastInvokeTime = now;
this.stats.invokedCount++;
// 记录历史
if (this.lastArgs) {
this.history.push({
args: [...this.lastArgs],
timestamp: now,
type: 'executed'
});
if (this.history.length > this.maxHistory) {
this.history.shift();
}
}
// 执行函数
this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);
this.lastArgs = null;
this.lastContext = null;
}
/**
* 调用节流函数
*/
call(...args) {
const now = Date.now();
this.stats.callCount++;
this.lastArgs = args;
this.lastContext = this;
// 检查是否在节流期内
const timeSinceLastInvoke = now - this.lastInvokeTime;
const isThrottled = timeSinceLastInvoke < this.interval;
if (isThrottled) {
this.stats.throttledCount++;
// 如果需要尾部执行
if (this.trailing) {
// 清除现有的尾部执行定时器
if (this.timer) {
clearTimeout(this.timer);
}
// 设置尾部执行定时器
const remainingTime = this.interval - timeSinceLastInvoke;
this.timer = setTimeout(() => {
if (this.lastArgs) {
this._invoke();
}
this.timer = null;
}, remainingTime);
}
return this.lastResult;
}
// 不在节流期内
if (this.leading) {
// 头部执行
this._invoke();
} else if (this.trailing) {
// 延迟执行
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
if (this.lastArgs) {
this._invoke();
}
this.timer = null;
}, this.interval);
}
return this.lastResult;
}
/**
* 取消尾部执行
*/
cancel() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.lastArgs = null;
this.lastContext = null;
}
/**
* 立即执行并取消尾部执行
*/
flush() {
if (this.lastArgs) {
this._invoke();
this.cancel();
}
return this.lastResult;
}
/**
* 判断是否有尾部待执行
*/
pending() {
return this.timer !== null;
}
/**
* 获取历史记录
*/
getHistory() {
return [...this.history];
}
/**
* 获取统计信息
*/
getStats() {
return { ...this.stats };
}
/**
* 重置状态
*/
reset() {
this.cancel();
this.history = [];
this.stats = {
callCount: 0,
invokedCount: 0,
throttledCount: 0
};
this.lastInvokeTime = 0;
this.lastResult = null;
}
}
进阶实现与组合优化
支持最大等待时间的防抖
支持最大等待时间的防抖,就是确保函数至少每隔 maxWait 时间执行一次:
function debounceMaxWait(fn, delay, maxWait) {
let timer = null;
let lastArgs = null;
let lastContext = null;
let lastInvokeTime = null;
let maxTimer = null;
const invoke = () => {
lastInvokeTime = Date.now();
fn.apply(lastContext, lastArgs);
lastArgs = null;
lastContext = null;
};
const startMaxWaitTimer = () => {
if (maxTimer) clearTimeout(maxTimer);
maxTimer = setTimeout(() => {
if (lastArgs) {
console.log('达到最大等待时间,强制执行');
invoke();
}
}, maxWait);
};
return function (...args) {
lastArgs = args;
lastContext = this;
// 清除现有延迟定时器
if (timer) {
clearTimeout(timer);
}
// 设置最大等待时间定时器
if (maxWait && !lastInvokeTime) {
startMaxWaitTimer();
}
// 设置新的延迟定时器
timer = setTimeout(() => {
invoke();
timer = null;
if (maxTimer) {
clearTimeout(maxTimer);
maxTimer = null;
}
}, delay);
};
}
动态调整延迟时间的防抖
根据调用频率动态调整等待时间:
function debounceAdaptive(fn, baseDelay, options = {}) {
const {
minDelay = 100,
maxDelay = 1000,
factor = 0.8
} = options;
let timer = null;
let lastArgs = null;
let lastContext = null;
let callTimes = [];
let currentDelay = baseDelay;
const calculateDelay = () => {
// 计算最近1秒内的调用频率
const now = Date.now();
callTimes = callTimes.filter(t => now - t < 1000);
const frequency = callTimes.length;
// 根据频率调整延迟
if (frequency > 10) {
// 高频调用,增加延迟
currentDelay = Math.min(currentDelay * (1 + frequency / 100), maxDelay);
} else if (frequency < 2) {
// 低频调用,减少延迟
currentDelay = Math.max(currentDelay * factor, minDelay);
}
return currentDelay;
};
return function (...args) {
callTimes.push(Date.now());
lastArgs = args;
lastContext = this;
if (timer) {
clearTimeout(timer);
}
const delay = calculateDelay();
console.log(` 当前延迟: ${Math.round(delay)}ms (调用频率: ${callTimes.length}/秒)`);
timer = setTimeout(() => {
fn.apply(lastContext, lastArgs);
timer = null;
}, delay);
};
}
实际应用场景实战
搜索框自动补全
class SearchAutoComplete {
constructor(options = {}) {
this.searchAPI = options.searchAPI || this.mockSearchAPI;
this.minLength = options.minLength || 2;
this.debounceDelay = options.debounceDelay || 300;
this.maxResults = options.maxResults || 10;
this.cacheResults = options.cacheResults !== false;
// 搜索缓存
this.cache = new Map();
// 创建防抖搜索函数
this.debouncedSearch = this.createDebouncedSearch();
// 请求计数器
this.requestCount = 0;
this.cacheHitCount = 0;
}
/**
* 模拟搜索API
*/
async mockSearchAPI(query) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 200));
// 模拟搜索结果
const results = [];
const prefixes = ['apple', 'banana', 'orange', 'grape', 'watermelon'];
for (let i = 1; i <= 5; i++) {
results.push({
id: i,
text: `${query} 结果 ${i}`,
category: prefixes[i % prefixes.length]
});
}
return results;
}
/**
* 创建防抖搜索函数
*/
createDebouncedSearch() {
const searchFn = async (query) => {
// 检查缓存
if (this.cacheResults && this.cache.has(query)) {
this.cacheHitCount++;
return this.cache.get(query);
}
// 执行真实搜索
this.requestCount++;
console.log(` 🌐 [请求#${this.requestCount}] "${query}"`);
try {
const results = await this.searchAPI(query);
// 存入缓存
if (this.cacheResults) {
this.cache.set(query, results);
// 限制缓存大小
if (this.cache.size > 50) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
}
return results;
} catch (error) {
console.error(`搜索失败: ${query}`, error);
return [];
}
};
// 使用完整版防抖
return debounceComplete(searchFn, this.debounceDelay, {
immediate: false,
maxWait: 1000
});
}
/**
* 用户输入处理
*/
onInput(query) {
// 忽略空查询
if (!query || query.length < this.minLength) {
console.log(' 查询太短,忽略');
return Promise.resolve([]);
}
// 执行防抖搜索
return this.debouncedSearch(query)
.then(results => {
const limited = results.slice(0, this.maxResults);
console.log(`返回 ${limited.length} 条结果`);
this.renderResults(limited);
return limited;
})
.catch(error => {
console.error('搜索失败:', error);
return [];
});
}
/**
* 渲染搜索结果
*/
renderResults(results) {
// 实际项目中这里会更新DOM
console.log(' 搜索结果:');
results.slice(0, 3).forEach((result, i) => {
console.log(` ${i + 1}. ${result.text}`);
});
if (results.length > 3) {
console.log(`... 等 ${results.length} 条`);
}
}
/**
* 清空缓存
*/
clearCache() {
this.cache.clear();
this.cacheHitCount = 0;
console.log('搜索缓存已清空');
}
/**
* 获取统计信息
*/
getStats() {
return {
requestCount: this.requestCount,
cacheHitCount: this.cacheHitCount,
cacheSize: this.cache.size,
pending: this.debouncedSearch.pending(),
debounceStats: this.debouncedSearch.getStats?.()
};
}
}
无限滚动加载
console.log('\n=== 无限滚动加载 ===\n');
class InfiniteScroll {
constructor(options = {}) {
this.loadMoreAPI = options.loadMoreAPI || this.mockLoadMoreAPI;
this.throttleInterval = options.throttleInterval || 200;
this.threshold = options.threshold || 200;
this.pageSize = options.pageSize || 20;
this.currentPage = 0;
this.hasMore = true;
this.isLoading = false;
this.items = [];
// 创建节流滚动处理函数
this.throttledScroll = this.createThrottledScroll();
// 记录最后一次滚动位置
this.lastScrollPosition = 0;
this.scrollHistory = [];
}
/**
* 模拟加载更多数据
*/
async mockLoadMoreAPI(page, pageSize) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 300));
// 模拟数据
const start = page * pageSize;
const items = [];
for (let i = 0; i < pageSize; i++) {
items.push({
id: start + i,
title: `项目 ${start + i}`,
content: `这是第 ${start + i} 个项目的内容`,
timestamp: Date.now()
});
}
// 模拟没有更多数据
const hasMore = page < 10;
return { items, hasMore };
}
/**
* 创建节流滚动处理函数
*/
createThrottledScroll() {
const scrollHandler = async (scrollTop, clientHeight, scrollHeight) => {
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
console.log(`滚动位置: ${scrollTop}, 距离底部: ${distanceFromBottom}px`);
// 记录滚动位置
this.lastScrollPosition = scrollTop;
this.scrollHistory.push({
position: scrollTop,
timestamp: Date.now()
});
// 限制历史记录大小
if (this.scrollHistory.length > 20) {
this.scrollHistory.shift();
}
// 检查是否需要加载更多
if (distanceFromBottom < this.threshold) {
await this.loadMore();
}
};
return throttleComplete(scrollHandler, this.throttleInterval, {
leading: true,
trailing: true
});
}
/**
* 处理滚动事件
*/
onScroll(event) {
const target = event.target;
const scrollTop = target.scrollTop || target.scrollingElement?.scrollTop || 0;
const clientHeight = target.clientHeight || window.innerHeight;
const scrollHeight = target.scrollHeight || document.documentElement.scrollHeight;
this.throttledScroll(scrollTop, clientHeight, scrollHeight);
}
/**
* 加载更多数据
*/
async loadMore() {
if (this.isLoading || !this.hasMore) {
console.log(`${this.isLoading ? '正在加载中' : '没有更多数据'}`);
return;
}
this.isLoading = true;
this.currentPage++;
console.log(`加载第 ${this.currentPage} 页数据...`);
try {
const result = await this.loadMoreAPI(this.currentPage, this.pageSize);
this.hasMore = result.hasMore;
this.items.push(...result.items);
console.log(`加载完成,当前总条目: ${this.items.length}`);
console.log(`还有更多: ${this.hasMore}`);
this.renderItems(result.items);
} catch (error) {
console.error('加载失败:', error);
this.currentPage--; // 回退页数
} finally {
this.isLoading = false;
}
}
/**
* 渲染新加载的项目
*/
renderItems(newItems) {
// 实际项目中这里会更新DOM
console.log('新增项目:');
newItems.slice(0, 3).forEach((item, i) => {
console.log(`${item.id}. ${item.title}`);
});
if (newItems.length > 3) {
console.log(`... 等 ${newItems.length} 条`);
}
}
/**
* 重置到顶部
*/
reset() {
this.currentPage = 0;
this.hasMore = true;
this.isLoading = false;
this.items = [];
this.scrollHistory = [];
console.log('滚动列表已重置');
}
/**
* 获取滚动统计
*/
getScrollStats() {
if (this.scrollHistory.length < 2) {
return { avgSpeed: 0 };
}
const recent = this.scrollHistory.slice(-10);
let totalSpeed = 0;
for (let i = 1; i < recent.length; i++) {
const distance = recent[i].position - recent[i - 1].position;
const timeDiff = recent[i].timestamp - recent[i - 1].timestamp;
const speed = distance / timeDiff; // px/ms
totalSpeed += speed;
}
return {
avgSpeed: totalSpeed / (recent.length - 1),
scrollCount: this.scrollHistory.length,
lastPosition: this.lastScrollPosition
};
}
}
最佳实践指南
防抖最佳实践
- 默认延迟时间:300-500ms(用户输入)、200-300ms(窗口调整)、1000ms(自动保存)
- 搜索框建议使用防抖,避免频繁请求
- 表单验证使用防抖,用户输完再验证
- 提交按钮使用防抖,防止重复提交
- 需要立即反馈的操作设置
immediate: true
节流最佳实践
- 滚动加载:200-300ms(平衡响应性和性能)
- 拖拽事件:16-33ms(约30-60fps)
- 窗口大小调整:100-200ms
- 游戏循环:使用
requestAnimationFrame替代定时器节流 - 频繁的状态更新:考虑使用
requestAnimationFrame
内存管理实践
- 组件卸载时取消未执行的防抖/节流
- 避免在全局作用域创建过多的防抖/节流函数
- 使用缓存时注意设置最大缓存大小
- 定期清理过期的缓存数据
调试技巧
- 添加日志追踪函数调用
- 记录调用历史便于回溯问题
- 使用 Stats 统计调用次数和节流情况
- 开发环境设置更短的延迟时间便于测试
防抖节流选择决策树
是否需要处理高频事件?
│
├─→ 是
│ │
│ ├─→ 是否需要关注最后一次执行?
│ │ │
│ │ ├─→ 是 → 使用防抖
│ │ │ │
│ │ │ ├─→ 搜索建议、自动保存、表单验证
│ │ │ └─→ 窗口调整、拖拽结束
│ │ │
│ │ └─→ 否 → 使用节流
│ │ │
│ │ ├─→ 滚动加载、拖拽中、动画帧
│ │ └─→ 游戏循环、鼠标移动
│ │
│ └─→ 是否需要立即执行?
│ │
│ ├─→ 是 → immediate: true
│ │ │
│ │ ├─→ 按钮提交(防止双击)
│ │ └─→ 数据埋点
│ │
│ └─→ 否 → immediate: false
│ │
│ ├─→ 搜索建议(避免每个字符都请求)
│ └─→ 自动保存(停止编辑后保存)
│
└─→ 否 → 不需要特殊处理
最终建议
- 不要盲目使用防抖/节流,先评估是否真的需要
- 根据用户体验选择合理的延迟时间
- 为防抖/节流函数命名时标明其特性
- 在类组件中绑定this时注意上下文
- 优先使用成熟的库实现(lodash、underscore)
- 理解原理,但不一定需要每次都自己实现
- 监控实际效果,根据数据持续优化
结语
防抖和节流是前端性能优化的基本工具,掌握它们不仅能提升应用性能,还能优化用户体验。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

{:width="60%"}