直接用 promise.all 会丢掉优先级,因其全量等待、无法中断旧任务或插入高优任务;需用带 priority、id、cancel 等元信息的任务对象构建优先队列,结合 abortcontroller 实现真正可中断、可插队的调度。

为什么直接用 Promise.all 做并发控制会丢掉优先级?
因为 Promise.all 本质是“全量等待”,它不关心每个请求的紧急程度,只要有一个失败就整体 reject,更无法动态插入高优任务并让低优任务暂停或降级。真实场景中,比如用户搜索关键词时触发的联想请求,新输入应立刻中断旧请求;又比如后台同步日志和前端埋点上报,前者必须比后者先发、且不能被挤占带宽。
所以得自己建队列,核心不是“限制并发数”,而是“按权重调度 + 可中断 + 可插队”。
PriorityQueue 要怎么存任务才支持动态插队和取消?
不能只存 Promise 构造函数,得封装成带元信息的对象。每个任务至少含:fn(实际执行函数)、priority(数值越大越优先)、id(唯一标识)、cancel(手动终止函数)。关键点是:取消不能靠抛错模拟,而要真正 abort 对应的 fetch 或 clearTimeout —— 所以 fn 必须返回一个带 abort 方法的对象,或者接收一个 signal 参数。
- 推荐用
AbortController统一管理中断,所有网络请求都传入signal -
priority不建议用字符串排序(如 "high"/"low"),统一转为数字,方便插入时二分查找位置 - 队列内部用数组 +
sort不够高效,小规模(Heap,可用@datastructures-js/priority-queue的MaxPriorityQueue
并发执行器怎么保证高优任务“插队成功”而不卡死?
常见错误是:插入高优任务后,直接 shift() 当前运行中的低优任务 —— 这会导致正在 fetch 的请求被 abort,但它的 catch 逻辑可能还没注册,造成未处理 rejection 报错。正确做法是:只允许新任务插到“待执行队列”头部,正在运行的任务继续跑完,但后续新任务一律等它们释放 slot 后再按新优先级排队。
- 维护两个队列:
running(当前执行中,只读)和pending(待调度,可排序插入) - 每次有任务完成,就从
pending取最高优先级的进running,而不是简单 FIFO - 暴露
cancel(id)方法时,只清除pending中的项;对running中的,调用其abort()并监听finally清理状态
示例片段:
const task = {
fn: () => fetch('/api', { signal }),
priority: 10,
id: 'search-abc',
abort: () => controller.abort()
};
权重值设多少才合理?不同业务怎么映射优先级?
没有全局标准值,但必须避免“所有任务都写 priority: 999”。建议按业务语义分层,例如:
- 用户交互强相关(搜索、表单提交)→
priority: 100 - 页面可见区域数据(首屏卡片)→
priority: 80 - 预加载/懒加载资源 →
priority: 40 - 离线日志同步、埋点上报 →
priority: 10
注意:权重差异太小(比如只差 1~2)在并发数少时几乎无效;差异太大(比如 1 和 10000)可能导致低优任务永远得不到执行。上线前一定要压测低优任务的平均等待时长,必要时加兜底超时自动提升优先级。
真正的难点不在排序算法,而在于 abort 的时机控制和错误边界处理——比如 fetch 被 abort 后,是否重试?重试要不要继承原 priority?这些得由业务方明确约定,库只提供可插拔的钩子。











