作为前端开发者,你是否遇到过这样的场景:用户在搜索框疯狂输入时,控制台疯狂打印请求日志;页面滚动时,控制台像机关枪一样输出信息,最后页面卡到怀疑人生?别慌,这时候防抖和节流就该登场了!今天咱们就好好聊聊这两位 "性能优化小能手"~
为什么需要防抖和节流?
先看个真实场景:做搜索建议功能时,用户每输入一个字符,我们就发一次请求到后端。如果用户打字速度快(比如一分钟打 60 个字),那 1 分钟就会发 60 次请求。这要是用户多了,服务器不得直接 "罢工"?
再比如滚动事件(scroll)、窗口大小改变(resize),这些事件触发的频率高到离谱。假设每次触发都要做 DOM 操作(重绘 / 重排),页面不卡顿才怪。
说白了,防抖和节流的核心目标就是:减少不必要的函数执行,平衡用户体验和性能消耗。
防抖(Debounce):"等你停了再说"
概念:一段时间内只执行最后一次
防抖的逻辑很简单:当函数被频繁触发时,只有在停止触发后等待一段时间,才会执行一次。如果在这段时间内又触发了,就重新计时。
比如打游戏时按技能,频繁按的时候技能不释放,松开手后才放 —— 这就是防抖的精髓~
实现原理与代码
先看个反例:未加防抖的输入事件,每输入一个字符就发请求:
// 模拟后端请求
function ajax(content) {
console.log('发送请求:', content);
}
// 未防抖:每次按键都触发
const inputA = document.getElementById('inputA');
inputA.addEventListener('keyup', (e) => {
ajax(e.target.value); // 疯狂触发,服务器压力山大
});
再看防抖后的实现。这里要用到高阶函数和闭包:高阶函数负责接收原函数和延迟时间,闭包用来保存定时器状态。
// 防抖函数优化版(修正原代码中fn.id的问题)
function debounce(fn, delay) {
let timer = null; // 用闭包保存定时器,避免多个实例冲突
return function(...args) {
// 每次触发都清除上一次的定时器
clearTimeout(timer);
// 重新设置定时器,延迟执行
timer = setTimeout(() => {
fn.apply(this, args); // 绑定this,确保原函数上下文正确
}, delay);
};
}
// 使用防抖包装请求函数
const debounceAjax = debounce(ajax, 300); // 300ms内不触发才执行
const inputB = document.getElementById('inputB');
inputB.addEventListener('keyup', (e) => {
debounceAjax(e.target.value); // 只有停止输入300ms后才发请求
});
这里修正了原代码中用
fn.id存储定时器的问题:如果同一个函数被多次防抖处理,fn.id会被共享,导致定时器冲突。用闭包中的timer变量更安全~
防抖的应用场景
- 搜索框输入联想(等用户输完一个词再发请求)
- 按钮点击防重复提交(避免用户疯狂点击)
- 文本编辑器自动保存(停止输入后保存)
节流(Throttle):"按固定节奏来"
概念:每隔一段时间必执行一次
节流和防抖不同:不管触发多频繁,每隔指定时间一定会执行一次。就像打游戏时的技能 CD,哪怕一直按,也只能按固定间隔释放。
比如页面滚动时计算元素位置,不需要每次滚动都算,每隔 100ms 算一次就够了。
实现原理与代码
节流的核心是记录 "上一次执行时间",每次触发时判断是否超过间隔:
// 节流函数
function throttle(fn, interval) {
let lastTime = 0; // 上一次执行时间
let timer = null; // 用于处理最后一次触发
return function(...args) {
const now = Date.now(); // 当前时间
// 如果距离上次执行不足间隔,设置定时器确保最后一次触发能执行
if (now - lastTime < interval) {
clearTimeout(timer);
timer = setTimeout(() => {
lastTime = now;
fn.apply(this, args);
}, interval);
} else {
// 超过间隔,直接执行
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用节流处理滚动事件
const throttleScroll = throttle((content) => {
console.log('处理滚动:', content);
}, 200); // 每隔200ms执行一次
window.addEventListener('scroll', () => {
throttleScroll('当前滚动位置:' + window.scrollY);
});
节流的应用场景
- 滚动加载(懒加载图片时,每隔一段时间判断一次元素位置)
- 窗口大小改变(
resize事件,每隔 100ms 重新计算布局) - 鼠标移动跟踪(如拖拽元素,固定频率更新位置)
防抖 vs 节流:怎么选?
用一张表总结两者的区别:
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 核心逻辑 | 停止触发后延迟执行一次 | 固定间隔内必执行一次 |
| 适用场景 | 输入联想、按钮防重复提交 | 滚动、resize、鼠标跟踪 |
| 执行时机 | 最后一次触发后延迟执行 | 间隔时间到就执行 |
简单说:需要 "等用户操作完" 才执行,用防抖;需要 "按节奏稳步执行",用节流~
防抖节流与闭包:天生一对
为什么防抖和节流能实现?核心是闭包。
闭包的特性是 "内部函数可以访问外部函数的变量,且这些变量不会被垃圾回收"。在防抖节流中:
-
防抖里的
timer(定时器 ID)被闭包保存,每次触发都能访问并清除上一次的定时器 -
节流里的
lastTime(上一次执行时间)被闭包保存,用来计算是否达到执行间隔
这也是为什么防抖节流函数要返回一个新函数 —— 新函数就是闭包,它 "记住" 了timer或lastTime的状态。
闭包的其他 "超能力"
除了防抖节流,闭包还有很多实用场景,顺带提几个:
1. 私有变量
通过闭包可以实现类的私有属性,避免外部直接修改:
function Book(title, author, year) {
// 私有变量(外部无法直接访问)
let _title = title;
let _author = author;
let _year = year;
// 公开方法(控制对私有变量的访问)
this.getTitle = () => _title;
this.updateYear = (newYear) => {
if (typeof newYear === 'number' && newYear > 0) {
_year = newYear;
} else {
console.error('年份必须是正数哦~');
}
};
}
const book = new Book('JS高级程序设计', 'Nicholas', 2011);
console.log(book._title); // undefined(私有变量访问不到)
book.updateYear(2024); // 正确修改
2. 绑定上下文
在事件监听或定时器中,经常遇到this丢失的问题,闭包 + 箭头函数 /bind 可以解决:
const person = {
name: '前端er',
sayHi: function() {
// 用箭头函数(继承外部this)
setTimeout(() => {
console.log(`${this.name} 说:Hi~`); // 正确输出"前端er 说:Hi~"
}, 1000);
}
};
person.sayHi();
总结
防抖和节流是前端性能优化的基础技能,核心是通过控制函数执行频率解决频繁触发问题。记住:
-
防抖:等用户 "停手" 再执行(最后一次有效)
-
节流:按固定节奏执行(间隔有效)
-
两者都依赖闭包保存状态,是闭包的典型应用
下次遇到频繁触发的事件,别再让函数 "疯狂加班" 了,用防抖节流给它 "降降压" 吧~