JavaScript 防抖与节流函数的通用实现
在 Web 开发中,我们经常需要处理高频触发的事件。搜索框实时输入、表单验证、窗口大小调整、滚动加载等场景,用户的操作可能在短时间内触发几十甚至上百次回调。如果每次触发都立即执行对应的逻辑,不仅会造成资源浪费,还可能导致页面卡顿、性能下降。
防抖(Debounce)和节流(Throttle)是两种解决这类问题的经典方案。它们的核心思想都是"控制函数执行频率",但具体策略有所不同。本文将手把手教你实现一个通用、可复用的防抖和节流工具函数,并详细解释它们的使用场景和区别。
1 防抖函数:等一等,再执行
1.1 防抖的核心理念
防抖的逻辑是:当事件被连续触发时,只有在停止触发一段时间后,函数才会真正执行。
你可以把它想象成"等电梯":有人按下电梯按钮后,如果又有人连续按,电梯会一直等待,直到没有人按了才开始关门。这个"等待"的时间就是防抖的延迟时间。
典型的应用场景包括:搜索框输入(等用户停止输入后再搜索)、窗口大小调整(等用户拖拽完了再计算布局)、表单提交(防止重复点击)。
1.2 防抖的通用实现
防抖函数的实现需要考虑几个关键因素:如何记录定时器、如何清除上一个定时器、如何传递上下文和参数。
/**
* 防抖函数
* @param {Function} func - 需要防抖处理的函数
* @param {number} wait - 延迟执行的时间(毫秒)
* @param {boolean} immediate - 是否立即执行( true: 触发即执行,false: 等最后一次触发后执行)
* @returns {Function} - 返回防抖处理后的函数
*/
function debounce(func, wait = 300, immediate = false) {
let timeoutId = null;
return function (...args) {
// 清除上一次的定时器
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
// 如果设置立即执行,且当前没有定时器,则立即执行
if (immediate && !timeoutId) {
func.apply(this, args);
}
// 设置新的定时器
timeoutId = setTimeout(() => {
// 如果不是立即执行,则在这里执行
if (!immediate) {
func.apply(this, args);
}
timeoutId = null;
}, wait);
};
}
1.3 防抖函数的使用示例
以下是一个搜索框的完整示例,演示防抖如何减少不必要的请求:
// 模拟搜索接口
function searchAPI(keyword) {
console.log(`正在搜索: ${keyword}`);
// 实际场景中这里会是 fetch 请求
}
// 创建防抖后的搜索函数,延迟 500 毫秒
const debouncedSearch = debounce(searchAPI, 500);
// 模拟用户连续输入
const searchInput = {
value: '',
addEventListener(event, callback) {
// 模拟用户快速输入多个字符
setTimeout(() => this.value = 'j', 0);
setTimeout(() => this.value = 'ja', 100);
setTimeout(() => this.value = 'jav', 200);
setTimeout(() => this.value = 'java', 300);
setTimeout(() => this.value = 'java', 500);
setTimeout(() => this.value = 'javasc', 600);
setTimeout(() => this.value = 'javascript', 800);
}
};
// 使用防抖函数处理输入
searchInput.addEventListener('input', debouncedSearch);
// 输出结果:只会看到一次 "正在搜索: javascript"
```
---
## 2 节流函数:固定频率执行
### 2.1 节流的核心理念
**节流的逻辑是**:无论事件被触发得多么频繁,函数都会按照固定的时间间隔执行。
你可以把它想象成"水龙头滴水":无论你怎么拧,水流都是按固定速度滴落。节流确保了函数执行的"下限"——它不会执行得比某个频率更快,但也不会完全停止。
典型的应用场景包括:滚动加载(固定时间间隔请求下一页数据)、高频点击(限制按钮点击频率)、游戏中的射击(固定射速)。
### 2.2 节流的通用实现
节流函数的实现有两种常见方式:时间戳方式和定时器方式。时间戳方式实现的节流会在高频触发时严格按照时间间隔执行最后一次,而定时器方式会在定时器结束后才允许下一次执行。
```javascript
/**
* 节流函数(时间戳方式)
* @param {Function} func - 需要节流处理的函数
* @param {number} interval - 固定执行的时间间隔(毫秒)
* @returns {Function} - 返回节流处理后的函数
*/
function throttle(func, interval = 300) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
// 如果距离上次执行的时间超过了间隔,立即执行
if (now - lastTime >= interval) {
lastTime = now;
func.apply(this, args);
}
};
}
```
```javascript
/**
* 节流函数(定时器方式,可选尾部补一次)
* @param {Function} func - 需要节流处理的函数
* @param {number} interval - 固定执行的时间间隔(毫秒)
* @param {boolean} trailing - 是否在最后触发一次(尾部执行)
* @returns {Function} - 返回节流处理后的函数
*/
function throttle(func, interval = 300, trailing = true) {
let timeoutId = null;
let lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
// 如果时间间隔已到,或者上次还没有执行过
if (remaining <= 0 || remaining > interval) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastTime = now;
func.apply(this, args);
} else if (trailing && !timeoutId) {
// 设置尾部定时器
timeoutId = setTimeout(() => {
lastTime = Date.now();
timeoutId = null;
func.apply(this, args);
}, remaining);
}
};
}
```
### 2.3 节流函数的使用示例
以下是一个无限滚动加载的示例,演示节流如何控制请求频率:
```javascript
// 模拟数据加载函数
function loadMoreData() {
console.log(`[${Date.now()}] 加载更多数据...`);
}
// 创建节流后的加载函数,每 1 秒最多执行一次
const throttledLoad = throttle(loadMoreData, 1000);
// 模拟用户快速滚动(每秒触发 10 次滚动事件)
let scrollCount = 0;
function simulateScroll() {
scrollCount++;
if (scrollCount <= 20) { // 模拟 20 次滚动事件
throttledLoad();
setTimeout(simulateScroll, 100); // 每 100 毫秒触发一次
}
}
// 开始模拟
simulateScroll();
// 输出结果:每秒最多执行一次,共执行约 10 次(因为 20 次 * 100ms = 2000ms)
3 防抖与节流的详细对比
理解两者的区别是正确使用它们的前提。以下表格从多个维度对比了两种技术:
| 对比维度 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 只执行最后一次 | 按固定间隔执行 |
| 等待策略 | 每次触发都重新计时 | 有固定的时间节奏 |
| 典型场景 | 搜索输入、窗口调整 | 滚动加载、射击频率 |
| 内存占用 | 一个定时器 | 一个或两个定时器 |
| 尾部执行 | 可配置 immediate 控制 | 可配置 trailing 控制 |
选择建议:当用户停止操作后才需要结果时选择防抖;当用户持续操作时需要按固定频率更新结果时选择节流。
4 完整工具模块:可直接复制使用
以下是一个封装好的工具模块,包含了本文介绍的所有函数。你可以直接复制到项目中作为工具库使用:
/**
* 防抖函数
* @param {Function} func - 需要防抖处理的函数
* @param {number} wait - 延迟执行的时间(毫秒)
* @param {boolean} immediate - 是否立即执行
* @returns {Function} - 返回防抖处理后的函数
*/
function debounce(func, wait = 300, immediate = false) {
let timeoutId = null;
return function (...args) {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (immediate && !timeoutId) {
func.apply(this, args);
}
timeoutId = setTimeout(() => {
if (!immediate) {
func.apply(this, args);
}
timeoutId = null;
}, wait);
};
}
/**
* 节流函数(时间戳方式)
* @param {Function} func - 需要节流处理的函数
* @param {number} interval - 固定执行的时间间隔(毫秒)
* @returns {Function} - 返回节流处理后的函数
*/
function throttle(func, interval = 300) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
func.apply(this, args);
}
};
}
/**
* 节流函数(定时器方式,支持尾部执行)
* @param {Function} func - 需要节流处理的函数
* @param {number} interval - 固定执行的时间间隔(毫秒)
* @param {boolean} trailing - 是否在最后触发一次
* @returns {Function} - 返回节流处理后的函数
*/
function throttle(func, interval = 300, trailing = true) {
let timeoutId = null;
let lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0 || remaining > interval) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastTime = now;
func.apply(this, args);
} else if (trailing && !timeoutId) {
timeoutId = setTimeout(() => {
lastTime = Date.now();
timeoutId = null;
func.apply(this, args);
}, remaining);
}
};
}
5 实际应用场景实战
5.1 场景一:搜索框防抖
搜索框是最典型的防抖应用场景。如果不进行防抖处理,用户每输入一个字符就会触发一次搜索请求,这会造成服务器压力过大,用户体验也很差。
class SearchBox {
constructor() {
this.debouncedSearch = debounce(this.fetchResults.bind(this), 300);
}
handleInput(event) {
const keyword = event.target.value.trim();
if (keyword.length > 0) {
this.debouncedSearch(keyword);
}
}
async fetchResults(keyword) {
console.log(`搜索关键词: ${keyword}`);
// 实际请求
const response = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
const results = await response.json();
this.renderResults(results);
}
renderResults(results) {
// 渲染结果到 DOM
console.log('渲染结果:', results);
}
}
5.2 场景二:滚动加载节流
无限滚动页面需要监听滚动事件来加载更多内容。如果不加节流,用户快速滚动时可能会在短时间内触发成百上千次请求。
class InfiniteScroll {
constructor(container, loader) {
this.container = container;
this.loader = loader;
this.isLoading = false;
this.page = 1;
// 使用节流,每 500 毫秒最多检查一次
this.throttledCheck = throttle(this.checkScroll.bind(this), 500);
this.init();
}
init() {
window.addEventListener('scroll', () => {
this.throttledCheck();
});
}
checkScroll() {
if (this.isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
// 距离底部 100 像素时触发加载
if (scrollHeight - scrollTop - clientHeight < 100) {
this.loadMore();
}
}
async loadMore() {
this.isLoading = true;
const data = await this.loader(this.page);
this.appendData(data);
this.page++;
this.isLoading = false;
}
appendData(data) {
console.log('追加数据:', data);
}
}
// 使用示例
// const scroll = new InfiniteScroll(document.getElementById('list'), loadPageData);
5. 场景三:按钮防重复提交
表单提交按钮需要防止用户重复点击,避免创建多条相同的记录。
function createSubmitHandler(submitButton) {
const debouncedSubmit = debounce(async function (formData) {
submitButton.disabled = true;
submitButton.textContent = '提交中...';
try {
await submitFormAPI(formData);
alert('提交成功!');
} catch (error) {
console.error('提交失败:', error);
} finally {
submitButton.disabled = false;
submitButton.textContent = '提交';
}
}, 2000, true); // immediate=true,立即执行一次,2 秒内不重复执行
return debouncedSubmit;
}
async function submitFormAPI(formData) {
return new Promise(resolve => setTimeout(resolve, 500));
}
6 进阶技巧与注意事项
6.1 处理 this 上下文
在事件处理函数中使用防抖或节流时,必须注意 this 指向问题。上述实现中使用 func.apply(this, args) 确保函数在正确的上下文中执行。如果省略这一步,事件处理函数中的 this 可能会指向 window 或 undefined。
6.2 传递事件对象
防抖和节流返回的函数使用 ...args 接收所有参数,这意味着事件对象会被正确传递。如果你的回调函数需要访问事件属性(如 event.target),可以直接在回调中使用。
6.3 取消防抖和节流
在某些场景下,你可能需要在特定条件下取消防抖或节流的效果。可以返回一个具有 cancel 方法的对象来实现这个功能:
function debounceWithCancel(func, wait = 300) {
let timeoutId = null;
const debounced = function (...args) {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, wait);
};
debounced.cancel = function () {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
return debounced;
}
6.4 内存管理
在单页应用中使用防抖或节流时,如果返回的函数不再需要被引用,应该确保定时器能够被正确清除。JavaScript 的垃圾回收机制会在函数没有引用时自动清理定时器,但在某些情况下(如组件卸载),手动调用 cancel 方法是一个好习惯。
7 总结
防抖和节流是处理高频事件的两个基础但强大的工具。防抖适合"等用户停下来"的场景,如搜索输入;节流适合"按固定节奏更新"的场景,如滚动加载。掌握这两种技术并理解它们的区别,能够帮助你写出更加性能友好的前端代码。
本文提供的实现方案都是通用且可复用的,你可以直接将其封装为工具模块,在项目需要时随时调用。记住一个原则:选择防抖还是节流,取决于你的业务场景需要"只执行最后一次"还是"按固定频率执行"。

暂无评论,快来抢沙发吧!