🚀 在前端开发中,我们经常需要处理频繁触发的事件,如滚动、调整窗口大小、按键操作等。如果不加以控制,这些事件可能导致大量回调函数执行,造成性能问题和糟糕的用户体验。今天,我要介绍两个解决这类问题的实用技巧:防抖(Debounce) 和 节流(Throttle)。
🔍 一、什么是防抖和节流
1.1 基本概 念
防抖和节流的本质都是优化高频率执行代码的手段。在浏览器中,像 resize
、scroll
、keypress
、mousemove
等事件可能会连续不断地触发,导致处理函数被频繁调用,消耗大量计算资源,降低页面性能甚至导致卡顿。
- 节流(Throttle): 控制函数执行频率,一段时间内只执行一次
- 防抖(Debounce): 延迟函数执行,多次触发时只执行最后一次(或第一次)
1.2 形象比喻:电梯的运行策略
想象一下每天上班时大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应:
🏢 电梯策略(假设超时设定为15秒):
- 节流版电梯:第一个人进来后,电梯15秒后准时运送。不管期间又有多少人进来,都不会影响这个15秒的定时。
- 防抖版电梯:第一个人进来后,电梯等待15秒。如果期间又有人进来,15秒等待重新计时。直到最后15秒没有新人进来,才开始运送。
🛠️ 二、实现方式
2.1 节流(Throttle)的实现
节流的核心是:在一段时间内,无论触发多少次函数,都只执行一次。
2.1.1 时间戳实现方式
使用时间戳实现的节流函数,会在触发事件时立即执行,但后续只有达到间隔时间才会再次执行。
function throttle(fn, delay = 500) {
let lastTime = 0;
return function(...args) {
const nowTime = Date.now();
if (nowTime - lastTime >= delay) {
fn.apply(this, args);
lastTime = nowTime;
}
};
}
特点:首次触发会立即执 行,停止触发后无法再次执行
2.1.2 定时器实现方式
使用定时器实现的节流函数,会在延迟后才第一次执行,之后按照间隔执行。
function throttle(fn, delay = 500) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
特点:首次触发会延迟执行,停止触发后依然会执行最后一次
2.1.3 结合版(更精确的控制)
结合时间戳和定时器,实现既立即执行,又能确保最后一次触发后还能执行的节流函数。
function throttle(fn, delay = 500) {
let timer = null;
let startTime = Date.now();
return function(...args) {
const currentTime = Date.now();
const remaining = delay - (currentTime - startTime);
const context = this;
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(() => {
fn.apply(context, args);
startTime = Date.now();
}, remaining);
}
};
}
2.2 防抖(Debounce)的实现
防抖的核心是:延迟执行,若在等待时间内再次触发,则重新计时。
2.2.1 基础版本
function debounce(fn, wait = 500) {
let timer = null;
return function(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
2.2.2 支持立即执行的防抖
有时我们希望第一次触发能立即执行,后续触发才进行防抖处理:
function debounce(fn, wait = 500, immediate = false) {
let timer = null;
return function(...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate) {
// 是否需要立即执行
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) {
fn.apply(context, args);
}
} else {
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
}
2.3 封装成工具函数
为了方便使用,我们可以将它们封装成工具函数,并添加取消功能:
// 节流工具函数
function throttle(fn, delay = 500) {
let timer = null;
let startTime = Date.now();
const throttled = function(...args) {
const currentTime = Date.now();
const remaining = delay - (currentTime - startTime);
const context = this;
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(() => {
fn.apply(context, args);
startTime = Date.now();
}, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timer);
timer = null;
startTime = Date.now();
};
return throttled;
}
// 防抖工具函数
function debounce(fn, wait = 500, immediate = false) {
let timer = null;
const debounced = function(...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) {
fn.apply(context, args);
}
} else {
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounced;
}
🔄 三、区别与选择
3.1 原理对比
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
执行时机 | 等待一段时间后执行 | 按照一定频率执行 |
触发频率变化时 | 重新计时 | 不影响执行频率 |
适用场景 | 输入框搜索、窗口调整大小 | 滚动加载、按钮点击 |
执行次数 | 连续触发只执行最后一次 | 每间隔时间执行一次 |
3.2 可视化比较
假设在2秒内,我们频繁触发一个事件10次,设置时间间隔为500ms:
- 节流:会按照500ms的频率执行,约执行4次
- 防抖:只会在最后一次触发后的500ms执行,只执行1次
假设触发事件的时间点如下所示:
------o------o--o--o-o-o-o--o------o------o---> 时间轴 ↑ ↑ ↑ ↑ ↑ 1 2 3 4 5
- 节流情况下的执行:1 3 5 (固定间隔执行)
- 防抖情况下的执行:5 (最后一次触发后执行)
🎯 四、应用场景
4.1 防抖(Debounce)应用场景
只关心最后一次操作,或者需要等待操作完全停止后再执行的场景
-
搜索框输入:用户输入完毕后再发送请求,避免频繁请求
const searchInput = document.getElementById('search');
const handleSearch = debounce(function(e) {
console.log('搜索:', e.target.value);
// API请求
}, 500);
searchInput.addEventListener('input', handleSearch); -
窗口大小调整:调整完成后再重新计算布局
const handleResize = debounce(function() {
console.log('窗口大小调整完成');
// 重新计算布局
}, 300);
window.addEventListener('resize', handleResize); -
表单验证:用户输入完成后再进行验证
const phoneInput = document.getElementById('phone');
const validatePhone = debounce(function(e) {
const phone = e.target.value;
const isValid = /^1[3-9]\d{9}$/.test(phone);
if (!isValid) {
showError('请输入正确的手机号');
} else {
hideError();
}
}, 400);
phoneInput.addEventListener('input', validatePhone);
4.2 节流(Throttle)应用场景
需要持续触发但又不希望频率过高的场景
-
滚动加载:滚动时每隔一段时间检查一次是否需要加载更多
const handleScroll = throttle(function() {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 100) {
console.log('距离底部100px,加载更多数据');
loadMoreData();
}
}, 300);
window.addEventListener('scroll', handleScroll); -
按钮点击:防止用户快速多次点击提交按钮
const submitBtn = document.getElementById('submit');
const handleSubmit = throttle(function(e) {
console.log('提交表单');
// 提交表单逻辑
}, 1000); // 设置较大的间隔,防止重复提交
submitBtn.addEventListener('click', handleSubmit); -
游戏中的按键响应:控制角色移动频率
const handleKeyDown = throttle(function(e) {
switch(e.key) {
case 'ArrowUp':
moveCharacter('up');
break;
case 'ArrowDown':
moveCharacter('down');
break;
// 其他按键处理
}
}, 100); // 每100ms响应一次按键
window.addEventListener('keydown', handleKeyDown);