在前端开发内容学习中,用 Promise 链给有状态写接口做串行队列:一种轻量并发控制方案是常见主题。很多人在阅读时会遇到概念分散、步骤不清和注意点难以归纳的问题。本文按照基础概念、操作流程和关键细节,对相关内容进行整理。
前端里经常遇到这样一类 写操作:

当这些写操作可能被连续、并发触发时(快速重复点击、设备高频输入、多个异步回调几乎同时到达),如果不加控制,容易出现:
busy 标志直接拒绝并发,导致部分操作被静默丢弃本文介绍一种用 Promise 链 实现串行队列的做法:不提高吞吐,而是保证同一资源上的写操作按顺序执行,且对上层调用尽量透明。
if (loading) return { ok: false, code: 'busy' }
loading = true
try { await api.write(...) }
finally { loading = false }
实现简单,但并发调用会被直接拒绝,容易丢操作。
核心想法:不把任务放进数组手动 shift,而是让每个新任务 then 挂到链尾。前一个回调结束后,下一个才会执行。
┌─────────┐
调用1 ──────────► │ task 1 │ ──► write ──► refresh
└────┬────┘
│ then
┌────▼────┐
调用2 ──────────► │ task 2 │ ──► write ──► refresh
└────┬────┘
│ then
┌────▼────┐
调用3 ──────────► │ task 3 │ ──► ...
└─────────┘
| 层 | 职责 |
|---|---|
| 入口(enqueue) | 挂到链尾,返回可 await 的 Promise |
| 执行体(worker) | 发请求、处理响应、刷新状态,try/finally 释放 loading |
在 Vue 2 等框架里,链指针每次入队都会更新。若放进 data(),会触发不必要的响应式开销。挂在实例上、不进 data 更合适:
created () {
this.writeChain = Promise.resolve()
}
enqueueWrite (payload, callbacks, options) {
if (!this.writeChain) {
this.writeChain = Promise.resolve()
}
const task = this.writeChain.then(() => {
return this._runWrite(payload, callbacks, options)
})
// catch 挂在「链尾更新」上,防止某次 reject 导致整条链断裂
this.writeChain = task.catch((e) => ({
ok: false,
code: 'chain_error',
msg: e && e.message
}))
return task
}
要点:
return task:调用方可 await 单次结果catch:失败被消化为普通返回值,后续任务仍能执行async _runWrite (payload, callbacks, options) {
this.inFlight = true
try {
// 1. 基于当前本地状态组装请求
// 2. await api.write(params)
// 3. 成功 → 回调 + await refresh()
// 4. 失败 → 错误处理 / 可选 refresh
// 5. return { ok, code, msg }
} catch (e) {
return { ok: false, code: 'error', msg: e.message }
} finally {
this.inFlight = false
}
}
try/finally 保证 loading 一定释放;队列保证同一时刻只有一个 worker 在跑。
sequenceDiagram
participant U as 调用方
participant Q as writeChain
participant W as _runWrite
participant API as 写接口
participant R as 刷新
U->>Q: 请求 A 入队
Q->>W: 执行 A
W->>API: write A
U->>Q: 请求 B 入队
U->>Q: 请求 C 入队
API-->>W: A 响应
W->>R: refresh
R-->>W: 完成
W-->>Q: A 结束
Q->>W: 执行 B
Note over W: B 基于 A refresh 后的状态
W->>API: write B
| 机制 | 作用 |
|---|---|
| Promise 链 | 决定何时执行——并发控制 |
| inFlight 标志 | 表达是否执行中——UI 反馈 |
try/finally | 保证标志一定被清除 |
链解决「丢请求」;标志解决「界面状态」。二者叠加,不互相替代。
需要用户确认后重试时,应 再次走入口入队,而不是在 worker 内裸递归:
this.enqueueWrite(payload, callbacks, options)
重试与外部新请求在同一队列里排序,不会插队破坏顺序。
可按场景提供 skipRefreshOnFail 等选项:失败时不立刻 refresh,由调用方统一回滚后再刷新。这是策略层选项,不改变串行模型。
优点
await局限
Map<resourceId, Promise>catch 会吞掉 rejection,单次结果要靠 await task 获取| 方案 | 并发控制 | 调用方体验 | 复杂度 |
|---|---|---|---|
| busy 互斥 | 拒绝并发 | 可能丢操作 | 低 |
| Promise 链 | 排队串行 | 透明、可 await | 低 |
| 显式队列 + worker | 排队串行 | 需取 ticket | 中 |
| 服务端版本号 | 冲突时重试 | 前端仍建议串行 | 中高 |
chain = chain.then(() => worker())catch 防断链return task 供单次 awaittry/finally 释放 loading这不是完整的「消息队列系统」,而是用 Promise 微任务语义实现的 最小 FIFO 串行器:把写操作从「互斥拒绝」升级为「自动排队、顺序提交、状态一致」。
控制的是正确性,不是吞吐量。若以后要「多资源并行、单资源串行」,把单链改成 Map<id, Promise> 即可,入口与 worker 的分层可以原样保留。