在前端开发中,像浏览器的 resize、scroll、keypress、mousemove 等高频事件触发时,会持续调用绑定的回调函数,这可能导致浏览器资源浪费、页面卡顿甚至性能下降。为优化此类场景的用户体验,防抖(Debounce)和节流(Throttle) 是两种常用的频率控制手段,通过限制回调函数的执行次数,减少无效计算,提升应用性能。
防抖
在事件被触发后,等待一段时间再执行回调,如果在这段时间内事件又被触发,则重新计时。确保事件停止触发后只执行一次回调函数。
防抖常用场景
防抖常用于输入框实时搜索、窗口调整大小、按钮多次点击等场景,避免频繁触发导致的性能问题。
搜索框的实时搜索建议(用户停止输入后请求):防止用户输入过程中频繁发送请求。 窗口大小调整时的重绘操作:避免因窗口频繁调整而持续触发重排重绘,影响性能。 按钮防连击:确保按钮不会因为用户快速连续点击而多次触发同一动作。
防抖的实现方式
延迟执行的防抖函数
function debounce(fn, delay) {
let timer = null;//初始化定时器
return function(...args) {//收集所有参数到args数组
//如果定时器已存在就清除它
if (timer) clearTimeout(timer);
// 设置新的定时器,在延迟delay毫秒后执行函数
timer = setTimeout(()=> {
fn.apply(this, args);//将参数数组都传递给原始函数fn
timer=null; // 清除定时器,重置timer,允许下次触发
}, delay);
}
}
首先防抖代码中的参数fn是需要执行的函数,delay是延迟执行的时间。
上面防抖代码中的剩余参数和箭头函数是ES6中的新特性,剩余参数用于收集函数调用时的所有参数,打包成一个数组。箭头函数则用于简化函数的写法,并自动绑定当前上下文中的this值。下面是普通函数重写的防抖函数(依旧延迟执行)
function debounce(fn, delay) {
let timer = null;
return function() {
const self = this; // 保存 this
const args = Array.from(arguments); // 将 arguments 转为数组
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(self, args); // 使用保存的 this 和参数
}, delay);
};
}
立即执行的防抖函数
function debounce(fn,delay){
let timer=null;
return function(...args){
//若定时器已存在就清除它
if(timer) clearTimeout(timer);
// 若是第一次调用或者定时器已经执行完毕,则立即执行函数
const shouldCallNow=!timer;
// 设置新的定时器,在延迟后充值状态
timer=setTimeout(()=>{
timer=null;
},delay);
// 立即执行函数
if(shouldCallNow) fn.apply(this,args);
timer=setTimeout(()=>{
fn.apply(this,args);
timer=null;
},delay);
};
}
节流
节流就是指连续触发事件但是在n秒中只执行一次函数。节流会稀释函数的执行频率(控制最小触发间隙)。
节流的常用场景
节流常用在滚动加载、高频事件(mousemove、touchmove)监听等场景,避免因事件触发频率过高导致性能问题。
节流的实现方式
时间戳节流函数
function throttle(fn, limit) {
//记录上次执行的时间戳
let lastTime = 0;
return function(...args) {
//获取当前时间戳
const now = Date.now();
//判断是否大于等于时间间隔
if (now - lastTime >= limit) {
//执行函数并传递参数
fn.apply(this, args);
//更新时间戳
lastTime = now;
}
}
初始化上次执行时间戳为0,第一次调用时,时间间隔是0,所以一定会立即执行,执行之后更新时间戳,确保两次执行之间间隔limit毫秒,停止触发后不会再次执行。
定时器节流函数
function throttleTimer(fn,limit){
let timer=null;
return function(...args){
// 定时器不存在就设置定时器
if(!timer){
timer=setTimeout(()=>{
fn.apply(this,args);
timer=null;
},limit);
}
};
}
定时器节流函数首次调用会等待limit毫秒后执行,停止触发后会在limit毫秒后执行。如果在定时器等待期间多次触发,只有第一次会被记录,也就是说定时器节流函数的执行间隔不严格。
综合版本节流函数
function throttle(func, limit) {
let lastExecTime = 0; // 记录上次执行时间
let timer = null; // 记录定时器状态
return function(...args) {
const now = Date.now();
const remaining = limit - (now - lastExecTime); // 计算距离下次执行还需的时间
// 如果距离上次执行时间超过了限制时间,则立即执行函数
if (remaining <= 0) {
// 如果有等待执行的定时器,则清除它
if (timer) {
clearTimeout(timer);
timer = null;
}
func.apply(this, args); // 立即执行
lastExecTime = now; // 更新上次执行时间
}
// 否则设置定时器,在剩余时间后执行函数
else if (!timer) {
timer = setTimeout(() => {
func.apply(this, args); // 延迟执行
lastExecTime = Date.now(); // 更新上次执行时间
timer = null; // 重置定时器状态
}, remaining);
}
};
}
综合版本的节流函数结合了时间戳和定时器的优点,首次调用会立即执行,停止触发后会在剩余时间后执行,如果在定时器等待期间多次触发,未超过limit首次触发创建定时器后续触发被忽略,最终执行一次(首次触发的参数);超过limit,清除定时器,立即执行当前触发的函数,执行一次。