JavaScript 防抖与节流完整指南
1. 概念对比
1.1 防抖(Debounce)
- 定义: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
- 场景:
- 搜索框输入联想
- 窗口大小调整
- 表单验证
- 按钮提交事件
1.2 节流(Throttle)
- 定义: 规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效
- 场景:
- 滚动事件处理
- 页面resize事件
- 射击游戏中的子弹发射
- 表单快速提交
1.3 区别示意
// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events: │─A─B─C─ │────D──│──E──│
Debounced: │────────│────D──│──E──│
// 节流:按照一定时间间隔执行
Input Events: │─A─B─C─D─E─F─G─│
Throttled: │─A─────D─────G─│
2. JavaScript 实现
2.1 防抖实现
// 基础版本
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 带立即执行选项的完整版本
function debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
2.2 节流实现
// 时间戳版本
function throttle(func, limit) {
let lastCall = 0;
return function executedFunction(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
func.apply(this, args);
lastCall = now;
}
};
}
// 定时器版本
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
3. 使用示例
3.1 防抖示例
// 搜索框输入防抖
const searchInput = document.getElementById('search');
const handleSearch = (event) => {
console.log('Searching:', event.target.value);
};
const debouncedSearch = debounce(handleSearch, 500);
searchInput.addEventListener('input', debouncedSearch);
// 窗口调整防抖
const handleResize = () => {
console.log('Window resized');
};
const debouncedResize = debounce(handleResize, 250);
window.addEventListener('resize', debouncedResize);
3.2 节流示例
// 滚动事件节流
const handleScroll = () => {
console.log('Scroll position:', window.scrollY);
};
const throttledScroll = throttle(handleScroll, 250);
window.addEventListener('scroll', throttledScroll);
// 按钮点击节流
const button = document.getElementById('submit-btn');
const handleClick = () => {
console.log('Button clicked');
};
const throttledClick = throttle(handleClick, 1000);
button.addEventListener('click', throttledClick);
4. TypeScript 实现
4.1 防抖 TypeScript 实现
// 防抖函数类型定义
type DebouncedFunction<T extends (...args: any[]) => any> = {
(...args: Parameters<T>): void;
cancel: () => void;
}
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
immediate: boolean = false
): DebouncedFunction<T> {
let timeout: NodeJS.Timeout | null = null;
function executedFunction(this: any, ...args: Parameters<T>): void {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
}
executedFunction.cancel = function() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
return executedFunction;
}
4.2 节流 TypeScript 实现
// 节流函数类型定义
type ThrottledFunction<T extends (...args: any[]) => any> = {
(...args: Parameters<T>): void;
cancel: () => void;
}
function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): ThrottledFunction<T> {
let inThrottle: boolean = false;
let lastTimeout: NodeJS.Timeout | null = null;
function executedFunction(this: any, ...args: Parameters<T>): void {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
lastTimeout = setTimeout(() => {
inThrottle = false;
}, limit);
}
}
executedFunction.cancel = function() {
if (lastTimeout) {
clearTimeout(lastTimeout);
inThrottle = false;
}
};
return executedFunction;
}
5. TypeScript 使用示例
5.1 React 组件中使用
import React, { useState, useCallback } from 'react';
interface SearchProps {
onSearch: (term: string) => void;
}
const SearchComponent: React.FC<SearchProps> = ({ onSearch }) => {
// 防抖搜索
const debouncedSearch = useCallback(
debounce((term: string) => {
onSearch(term);
}, 500),
[onSearch]
);
return (
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
};
// 节流滚动
const ScrollComponent: React.FC = () => {
const [scrollPosition, setScrollPosition] = useState(0);
const throttledScroll = useCallback(
throttle(() => {
setScrollPosition(window.scrollY);
}, 250),
[]
);
React.useEffect(() => {
window.addEventListener('scroll', throttledScroll);
return () => {
window.removeEventListener('scroll', throttledScroll);
throttledScroll.cancel();
};
}, [throttledScroll]);
return <div>Scroll Position: {scrollPosition}</div>;
};
5.2 Vue 组件中使用
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const searchTerm = ref('');
const debouncedSearch = debounce((term: string) => {
console.log('Searching for:', term);
}, 500);
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
searchTerm.value = target.value;
debouncedSearch(target.value);
};
return {
searchTerm,
handleInput
};
}
});
6. 最佳实践
6.1 防抖使用建议
- 设置合适的延迟时间
- 考虑是否需要立即执行
- 注意清理工作
- 在组件卸载时取消防抖
6.2 节流使用建议
- 根据实际场景选择合适的时间间隔
- 考虑是否需要首次执行
- 注意内存泄漏
- 在高频事件中使用
6.3 选择建议
- 选择防抖:
- 连续的事件响应只需要执行最后一次
- 需要等待用户输入完成后再执行
- 选择节流:
- 需要保持一定的执行频率
- 需要限制事件触发频率
7. 性能考虑
- 内存使用
- 避免在组件渲染时创建新的防抖/节流函数
- 使用 useCallback 或 useMemo 缓存函数
- 执行时机
- 考虑是否需要在首次触发时立即执行
- 考虑是否需要在等待结束后执行最后一次
- 清理工作
- 组件卸载时清除定时器
- 取消未执行的回调
- 类型安全
- 使用 TypeScript 确保类型安全
- 正确处理 this 绑定和参数类型