🚀 在前端开发中,我们经常需要处理频繁触发的事件,如滚动、调整窗口大小、按键操作等。如果不加以控制,这些事件可能导致大量回调函数执行,造成性能问题和糟糕的用户体验。今天,我要介绍两个解决这类问题的实用技巧:防抖(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;
}