写一个可中断的异步任务队列,解决前端并发调度难题
前端并发调度的难题,很多时候不是“怎么同时发更多请求”,而是“怎么在正确的时候停掉不该继续的任务”。用 `Promise.all` 一把梭,页面看起来很快,但搜索框连续输入三个关键词,三个请求乱序返回,用户看到的是旧结果;路由已经切走,上一个页面的请求还在跑;批量上传到一半,用户点了取消,请求却依然占着连接数。问题不在异步本身,而在缺少一个可中断、可调度的异步任务队列。
并发控制不等于 Promise.all
浏览器对同域请求有连接数限制,主线程也只有一条。无限制地并发,只会让关键任务排在后台任务后面,甚至造成内存和连接池浪费。队列的第一层价值是限制并发数,第二层价值是决定“谁先跑”,第三层价值是决定“谁该停”。如果只能排队不能取消,队列只是一个延迟执行的容器;只有把中断能力设计进去,它才真正解决前端并发调度难题。
把“中断”设计成一等公民
异步任务的中断不是线程 kill,而是协作式取消。浏览器里最合适的协议是 `AbortController` 和 `AbortSignal`:`fetch` 原生支持,自定义异步函数也可以接收 signal。任务函数统一写成 `(signal: AbortSignal) => Promise<T>`,在关键节点检查 `signal.aborted`,或者用 `signal.addEventListener('abort', ...)` 清理资源。取消后,队列要拒绝该任务的 Promise,并抛出统一的 `AbortError`,避免调用方把取消误判成业务失败。
一个最小可中断队列
下面是一个简化实现,重点看状态流转和取消传播:
type Task<T> = {
id: number;
run: (signal: AbortSignal) => Promise<T>;
priority: number;
resolve: (v: T) => void;
reject: (e: unknown) => void;
controller: AbortController;
};
class AsyncQueue {
private queue: Task<any>[] = [];
private running = 0;
constructor(private concurrency = 3) {}
add<T>(
run: (signal: AbortSignal) => Promise<T>,
opts: { priority?: number; signal?: AbortSignal } = {}
) {
return new Promise<T>((resolve, reject) => {
const controller = new AbortController();
const task: Task<T> = {
id: Date.now() + Math.random(),
run,
priority: opts.priority ?? 0,
resolve,
reject,
controller,
};
const abort = () => {
this.queue = this.queue.filter(t => t.id !== task.id);
controller.abort();
reject(new DOMException('Task aborted', 'AbortError'));
};
if (opts.signal) {
if (opts.signal.aborted) return abort();
opts.signal.addEventListener('abort', abort, { once: true });
}
this.queue.push(task);
this.queue.sort((a, b) => b.priority - a.priority);
this.next();
});
}
private next() {
while (this.running < this.concurrency && this.queue.length) {
const task = this.queue.shift()!;
this.running++;
task.run(task.controller.signal)
.then(task.resolve)
.catch(task.reject)
.finally(() => {
this.running--;
this.next();
});
}
}
}
运行中的任务通过 `controller.abort()` 通知内部停止;排队中的任务直接移出队列。调用方可以传入自己的 signal,也可以用返回的 Promise 配合事件取消。真正的中断效果,取决于任务函数是否响应 signal。
调度策略决定体验
优先级要贴近用户意图:输入框的搜索建议优先于列表预取,当前路由的数据优先于埋点上报。超时可以用 `AbortController` 加定时器实现,超时即取消。重试也要可取消,否则用户离开后还在后台反复请求。为了避免低优先级任务饿死,可以给等待时间长的任务提升优先级。并发数最好可动态调整,比如首屏关键阶段降到 2,空闲阶段升到 5。
几个容易踩的坑
取消后仍然 resolve,会让调用方拿到过期数据;忘记移除 signal 监听,可能造成内存泄漏;任务内部不检查 `signal.aborted`,取消就只是表面功夫;队列无限增长,会吃掉内存。还有一点,取消不是万能的,写操作接口要保证幂等,避免撤销和重试造成重复提交。
可中断的异步任务队列,本质上是在调度层统一管理并发、优先级和生命周期。前端体验的稳定性,不只来自“跑得快”,更来自“该停的时候停得住”。当页面交互越来越复杂,把取消和调度收拢到一个队列里,往往比在每个组件里写防抖、节流和竞态判断更可靠。
转载请注明出处,版权归原作者所有。
管理员



