在前端开发中,高频事件(如输入、滚动、窗口缩放)若不加控制,极易引发性能问题。为应对这一挑战,防抖(debounce) 与 节流(throttle) 成为必备工具。
一、防抖:每次触发都重置定时器
假设我们要实现一个功能:用户在输入框打字时,只有当他停止输入超过 1 秒,才发送请求。
第一步:我需要延迟执行
显然,要用 setTimeout:
scss
setTimeout(() => {
ajax(value);
}, 1000);
第二步:但如果用户继续输入,之前的请求就不该发
→ 所以必须取消之前的定时器,再建一个新的。
这就要求我们保存定时器 ID:
scss
let timerId;
// 每次触发时:
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
ajax(value);
}, 1000);
第三步:处理 this 和参数
因为 ajax 可能依赖上下文或多个参数,不能直接写死。我们需要在触发时捕获当前的 this 和 arguments:
javascript
function debounce(fn, delay) {
let timerId;
return function(...args) {
const context = this;
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
到此,防抖完成。它的全部逻辑就源于一句话: "每次触发,先删旧定时器,再建新定时器。"
所谓"停手后执行",只是这种操作的自然结果。
二、节流:控制执行频率,必要时预约补发
现在需求变了:不管用户多快输入,每 1 秒最多只发一次请求,且最后一次输入不能丢。
第一步:我能立即执行吗?
用时间戳判断是否已过 delay:
ini
const now = Date.now();
if (now - last >= delay) {
fn();
last = now; // 记录执行时间
}
这能保证最小间隔,但有个致命缺陷:如果用户快速输入后立刻停止,最后一次可能永远不会执行。
第二步:如何不丢尾?
→ 在冷却期内,预约一次未来的执行 。这就要用到 setTimeout。
于是逻辑分裂为两条路径:
- 路径 A(可立即执行) :时间到了,马上执行,更新
last - 路径 B(还在冷却) :清除之前的预约,重新预约一次执行
第三步:管理预约定时器
我们需要一个变量 deferTimer 来保存预约任务的 ID:
ini
let last = 0;
let deferTimer = null;
当处于冷却期时:
scss
clearTimeout(deferTimer); // 清除旧预约
deferTimer = setTimeout(() => {
last = Date.now(); // 关键:这次执行也要记录时间!
fn.apply(this, args);
}, delay - (now - last)); // 精确计算剩余等待时间
第四步:整合逻辑
ini
function throttle(fn, delay) {
let last = 0;
let deferTimer = null;
return function(...args) {
const context = this;
const now = Date.now();
if (now - last >= delay) {
// 路径 A:立即执行
last = now;
fn.apply(context, args);
} else {
// 路径 B:预约执行
if (deferTimer) clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = Date.now(); // 必须更新!
fn.apply(context, args);
}, delay - (now - last));
}
};
}
节流的核心,是两种执行方式的协同:
- 立即执行靠时间戳判断
- 补发执行靠
setTimeout预约
而两者共享同一个last状态,确保整体节奏不乱。
三、对比总结:防抖 vs 节流的机制差异
| 维度 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 核心操作 | 每次触发都 clearTimeout + setTimeout |
冷却期内 clearTimeout + setTimeout,否则立即执行 |
| 状态变量 | 仅需 timerId |
需 last(时间) + deferTimer(预约ID) |
| 执行特点 | 只执行最后一次 | 固定间隔执行,且不丢尾 |
| 适用场景 | 搜索建议、表单校验 | 滚动加载、按钮限频、实时位置上报 |