tag:前端防抖 | 请求合并 | Vue | 面试题 | 高级优化
“你知道在用户频繁点击按钮时,怎么把这些请求压缩成一次提交吗?” 面试官盯着我,轻轻点了点桌面。我知道,这道题不止考防抖,它想要的,是架构级的思考方式。
有一次做一个后台运营功能,页面上有个“批量发布”按钮。用户在点击时,会发起一个 API 请求,把选中的文章一键发布。
结果上线后发现,有运营同事习惯性猛点按钮……短短几秒钟,服务器收到十几个重复请求,导致文章被重复推送,还顺带炸了个 Redis 队列。
当时我用了节流+disabled 的土办法解决了问题。但直到有天面试被问到“如何把多次点击合并成一次请求”,我才意识到:节流只是权宜之计,要真正优雅地处理这种情况,得升维思考。

大多数人会想到节流(throttle)或防抖(debounce):
const submit = debounce(() => {
api.post('/submit', data);
}, 500);但面试官真正想听到的,不是这一行代码,而是为什么这样做,以及还有没有更优解。
于是我在项目里逐步尝试了几种方式,每一步都更抽象、更通用。
适用于输入框搜索、按钮点击。比如:
const submitForm = debounce(() => {
axios.post('/submit', getFormData());
}, 800);优点是简单直观,缺点是粒度太细,不适合组件通信或高频场景。
这是我后来面试讲到的重点方案,很多人没讲到这里,面试官就已经开始点头了。
// 单例请求管理器
class RequestMerger {
constructor() {
this.queue = [];
this.timer = null;
}
enqueue(data) {
this.queue.push(data);
if (!this.timer) {
this.timer = setTimeout(() => {
this.flush();
}, 300); // 合并时间窗口
}
}
flush() {
const merged = [...this.queue];
this.queue = [];
this.timer = null;
axios.post('/api/submit', merged)
.then(res => {
// TODO: 回调处理
});
}
}
// 使用方式
const merger = new RequestMerger();
function onClickSubmit(itemData) {
merger.enqueue(itemData);
}这个设计和“打包发货”很像:每次有人下单就放进筐里,过几百毫秒统一寄出。
如果你的场景是多个组件或模块在几乎同时提交请求,比如:
你可以用事件总线或全局管理器封装:
// submitBus.js
const listeners = [];
let timer = null;
let queue = [];
export function submitData(data) {
queue.push(data);
if (!timer) {
timer = setTimeout(() => {
const toSubmit = [...queue];
queue = [];
timer = null;
axios.post('/api/mergeSubmit', toSubmit).then(res => {
listeners.forEach(cb => cb(res));
});
}, 200);
}
}
export function onResponse(cb) {
listeners.push(cb);
}因为它能看出你对性能优化、用户体验、系统设计的理解深度。
答得好的人,通常会延伸到:
而这些,恰恰是中高级前端工程师的分水岭。
面试时我补了一句:
“服务端收到的是一个数组,我们可以在 controller 层拆分每个请求数据,再并发处理,再统一返回。比如可以用 Promise.all 或异步队列去处理每一项请求。这样既能减轻前端负担,又能保持业务一致性。”
面试官点了点头,说了一句:
“你是第一个讲到这一层的人。”