防抖与节流
1. 概念与区别
(1) 防抖
定义:在事件被触发 delay 毫秒后才执行。如果在该时间内再次触发,则重新计时。
场景:
- 用户在输入框搜索(停止输入后再发请求)。
- 自动保存(停止编辑后再保存)。
- 窗口调整
resize (调整过程中不计算,停止后计算)。
核心思想:对于连续的事件,只执行最后一次。
(2) 节流
定义:规定在单位时间内(如 1 秒)只能触发一次。如果该时间内被触发多次,只有一次生效。
场景:
- 滚动条
scroll 监听(可设置每 100ms 计算一次位置)。
- 按钮防止重复点击。
核心思想:对于连续的事件,稀释执行频率(每隔多少时间执行一次)。
2. 手写防抖与节流
面试时,不仅需要说出区别,更需要现场手写代码。
(1) 手写防抖
/**
* @param {Function} func 需要防抖的函数
* @param {Number} delay 延迟时间 (ms)
* @param {Boolean} immediate 是否立即执行一次
*/
function debounce(func, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
func.apply(this, args);
}
timer = setTimeout(() => {
if (!immediate) func.apply(this, args);
timer = null; // 执行完置空,下次可再次触发 immediate
}, delay);
};
}
(2) 手写节流
这里提供两种方案:时间戳版(立即执行,但在最后一次后不会再执行)和 定时器版(延迟执行,但在最后一次后会补执行)。面试写出简单版即可。
组合版 (推荐):
/**
* @param {Function} func 需要节流的函数
* @param {Number} wait 间隔时间 (ms)
*/
function throttle(func, wait) {
let timer = null;
let lastTime = 0;
return function (...args) {
const now = Date.now();
// 下次触发剩余时间
const remaining = wait - (now - lastTime);
// 如果时间间隔到了,或者系统时间被调回去了
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
func.apply(this, args);
} else if (!timer) {
// 还在时间内,设置定时器在剩余时间结束后执行(补刀)
timer = setTimeout(() => {
lastTime = Date.now(); // 注意这里更新时间
timer = null;
func.apply(this, args);
}, remaining);
}
};
}
3. 防抖在 AI 对话框中的应用
场景:用户停止输入后再请求辅助能力
AI 对话框中,用户每输入一个字符都会触发 input 事件。如果每次输入都请求服务端,容易产生大量无效请求,并且会让旧请求和新请求同时返回,造成提示内容闪烁或覆盖。
防抖适合处理“用户停止输入后再执行”的任务,例如:
- 生成 Prompt 补全或问题建议。
- 根据输入内容匹配历史问题、知识库或指令模板。
- 停止编辑后自动保存草稿。
- 停止输入后计算 Token 数量、上下文长度和费用预估。
但“发送消息”通常不应该使用防抖。用户点击发送后应立即提交,重复发送应通过按钮禁用、请求状态或幂等键处理。
实现:防抖 + 取消旧请求
function createPromptSuggestion() {
let timer = null;
let controller = null;
return function suggest(prompt, onResult, onError) {
clearTimeout(timer);
controller?.abort();
// 输入为空时直接清空建议,不请求服务端
if (!prompt.trim()) {
onResult([]);
return;
}
timer = setTimeout(async () => {
controller = new AbortController();
try {
const response = await fetch("/api/prompt-suggestions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
onResult(data.suggestions || []);
} catch (error) {
// 用户继续输入导致的取消,不需要提示错误
if (error.name !== "AbortError") {
onError?.(error);
}
}
}, 400);
};
}
const suggest = createPromptSuggestion();
const input = document.querySelector("#prompt-input");
input.addEventListener("input", (event) => {
suggest(
event.target.value,
(suggestions) => renderSuggestions(suggestions),
(error) => showSuggestionError(error)
);
});
为什么要取消旧请求?
仅使用 setTimeout 只能减少请求次数,不能处理已经发出的请求。例如:
用户输入 A → 请求 A 发出
用户输入 AB → 请求 B 发出
请求 B 先返回 → 页面显示 B 的建议
请求 A 后返回 → 旧结果覆盖了 B
使用 AbortController 可以在新输入到来时取消旧请求。即使某些服务端已经处理了旧请求,前端也不会再使用被取消请求的结果。对于无法取消的请求,还应增加请求序列号,只接受最后一次请求的响应。
参数选择与注意事项
- 防抖时间通常取
300~500ms:太短会产生较多请求,太长会让输入体验变差。
- 处理输入法时,要关注
compositionstart 和 compositionend,避免中文输入过程中间状态触发请求。
- 流式输出不适合用防抖控制每个 Token 的渲染,可以使用节流或批量刷新,保证界面流畅。
- 草稿自动保存可以使用更长的防抖时间,例如
800~1500ms,并在页面离开前再主动保存一次。
- 请求接口应设置超时、重试策略和鉴权;发送消息等非幂等操作不能仅靠防抖防重复。
4. 面试考点
- this 指向问题:回调函数必须使用
function 关键字或者箭头函数通过 apply 绑定正确的 this。
- 参数透传:使用
...args 并通过 apply 传递给原函数。
- 立即执行:防抖和节流都可以通过
immediate 选项控制首次是否立即触发。
- 取消功能:在返回的函数上挂载
.cancel() 方法清除定时器。
5. 总结
- 防抖:最后一次说了算。(输入框搜索)
- 节流:按节奏执行。(滚动监听)