防抖与节流:前端性能优化的两大利器
在现代 Web 开发中,用户交互越来越频繁,而每一次交互都可能触发复杂的逻辑处理或网络请求。如果不加以控制,这些高频操作会带来严重的性能问题。为此,防抖(Debounce) 与 节流(Throttle) 成为了前端开发中不可或缺的性能优化手段。
本文将结合一段实际代码和详细注释,深入浅出地讲解防抖与节流的核心思想、实现方式以及适用场景,并重点解析其中的关键逻辑。
一、为什么需要防抖和节流?
设想这样一个场景:用户在搜索框中输入关键词,每按一次键就发起一次 AJAX 请求获取搜索建议。如果用户快速输入“react”,那么会依次触发 r → re → rea → reac → react 五次请求。
-
问题1:请求开销大
每次请求都需要消耗带宽、服务器资源,甚至可能造成接口限流。 -
问题2:用户体验差
如果请求响应慢,旧的请求结果可能会覆盖新的输入内容,导致显示错乱。
因此,我们需要一种机制来减少不必要的执行次数,只保留关键的操作。这就是防抖和节流要解决的问题。
防抖:在一定时间内,只执行最后一次操作。
节流:每隔固定时间,最多执行一次操作。
二、防抖(Debounce)——“只认最后一次”
1. 核心思想
无论执行多少次,只执行最后一次。
就像王者荣耀中的“回城”技能:如果你在回城过程中被攻击,回城会被打断并重新计时。只有当你完整地等待一段时间后,回城才会真正生效。
2. 代码实现与闭包应用
// 高阶函数 参数或者返回值是函数 (返回值是函数 -> 闭包)
function debounce(fn, delay) {
var id; // 自由变量,闭包保存
return function(args) {
if (id) clearTimeout(id); // 清除已有定时器,重新计时
var that = this; // 保存 this 上下文
id = setTimeout(function() {
fn.call(that, args); // 延迟执行原函数 并绑定正确的this和参数
}, delay);
// 这样只有最后一次触发后等待delay毫秒后才会真正执行
};
}
关键点解析:
-
闭包的作用:
id是一个自由变量,被返回的函数所引用,从而在多次调用之间保持状态。这使得每次触发都能访问并清除上一次的定时器。 -
clearTimeout(id):确保只有最后一次触发后的delay时间才会真正执行函数。 -
this和参数传递:通过call或apply确保原函数在正确的上下文中执行,并传入正确的参数。
3. 使用示例
const inputb = document.getElementById('debounce');
let debounceAjax = debounce(ajax, 200);
inputb.addEventListener('keyup', function(e) {
debounceAjax(e.target.value);
});
用户快速输入时,只有停止输入 200ms 后,才会发送最终的完整关键词请求,极大减少了无效请求。
三、节流(Throttle)——“冷却期内不执行,但最后补一次”
1. 核心思想
每隔一定时间,最多执行一次。
但注意:我们实现的是带尾随执行(trailing)的节流,即在冷却期结束后,如果期间有触发,会补一次执行。
就像技能有 CD(冷却时间),但如果你在 CD 期间一直按技能,CD 结束后会自动释放一次。
2. 代码实现与“尾随执行”逻辑
function throttle(fn, delay) {
let last, deferTimer; // last上一次执行事件 deferTimer延迟执行的定时器
return function() {
let that = this;
let _args = arguments; // 类数组对象 保存所有参数
let now = +new Date(); // 拿到当前时间戳 +强制类型转换 毫秒数
if (last && now < last + delay) {
// 处于冷却期 上次执行时间存在 且当前时间还没到下次允许执行的时间
clearTimeout(deferTimer);
deferTimer = setTimeout(function() {
last = now;
fn.apply(that, _args);
}, delay);
} else {
// 已过冷却期,立即执行
last = now;
fn.apply(that, _args);
}
};
}
重点解析 if (last && now < last + delay) 分支:
-
条件成立含义:已经执行过至少一次(
last存在),且当前时间距离上次执行不足delay毫秒 → 正处于冷却期。 - 但不能忽略这次触发!因为这可能是用户最后一次有效操作(比如完整输入了“react”)。
- 所以我们设置一个延迟定时器,计划在冷却期结束后执行。
-
clearTimeout(deferTimer)的作用:用户可能在冷却期内多次触发,但我们只关心最后一次,所以每次都要清除旧的定时器,只保留最新的。
3. 为什么需要“尾随执行”?
核心原因:避免丢失最后一次有效操作。
假设用户想搜 “react”,在 200ms 内快速打完,而节流 delay = 500ms:
-
简单节流(无尾随) :
-
r(0ms)→ 立即执行 -
re(100ms)→ 被忽略 -
rea(150ms)→ 被忽略 -
react(200ms)→ 被忽略
→ 用户停止输入 但永远不会发送'react' 搜索框显示的是r的结果 而不是用户真正想搜的react
-
-
带尾随的节流:
-
r(0ms)→ 立即执行(last = 0) -
re(100ms)→ 冷却期,设 timer(600ms 执行) -
rea(150ms)→ 更新 timer(650ms) -
react(200ms)→ 更新 timer(700ms)
→ 用户停止输入后,在 700ms 自动执行ajax('react'),结果正确!
-
4. 什么时候不需要尾随?
按钮防连点:用户点击“提交”按钮,你希望 2 秒内只能点一次。
这种情况下,不需要在 2 秒后自动再提交一次!此时应使用无尾随的简单节流。
四、防抖 vs 节流:如何选择?
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 停止触发后 delay ms 执行 |
每隔 delay ms 最多执行一次 |
| 是否保证最后一次 | ✅ 是 | ✅(带尾随时) |
| 典型场景 | 搜索建议、窗口 resize | 滚动加载、鼠标移动、按钮点击(防连点) |
| 类比 | 回城技能(被打断重计时) | 技能 CD(冷却后可再放) |
- 搜索建议 → 用防抖:用户输入是连续的,我们只关心最终结果。
- 滚动加载 → 用节流:用户持续滚动,我们需要定期检查是否到底部,不能等到停止滚动才加载。
五、总结
防抖和节流虽然都是用于限制函数执行频率,但它们的触发逻辑和适用场景截然不同:
- 防抖强调“只执行最后一次”,适用于用户意图明确、操作连续的场景,如搜索、表单校验。
- 节流强调“定期执行”,适用于高频但需周期性响应的场景,如滚动、拖拽、游戏帧更新。
而我们在实现节流时,特别加入了尾随执行(trailing) 机制,这是为了兼顾性能与用户体验——既避免了过度请求,又确保不会丢失用户的最终操作。
正如注释中所说:
“核心原因:避免丢失最后一次有效操作。”
通过合理运用闭包、定时器和上下文绑定,我们不仅实现了功能,还保证了代码的健壮性和可复用性。这些技巧,正是前端工程师在性能优化道路上的必备武器。
小提示:在实际项目中,Lodash 等工具库已提供了成熟的 debounce 和 throttle 实现,支持更多选项(如 leading、trailing 开关)。但理解其底层原理,才能在复杂场景中灵活应对。
希望本文能帮助你更清晰地掌握防抖与节流的本质。欢迎在评论区分享你的使用经验!