深色模式
日常开发中 Promise 的基础用法(.then、.catch、.finally)已经很熟了,但实际项目中经常遇到一些进阶场景:并发控制、超时处理、串行执行、全部完成(不管成功失败)等。这篇文章整理了我在项目中积累的 Promise 高级用法。
Promise.all vs Promise.race
先回顾两个基础 API 的区别:
javascript
// Promise.all:全部成功才成功,一个失败就失败
const results = await Promise.all([
fetch('/api/users'),
fetch('/api/orders'),
fetch('/api/products')
])
// results 是三个响应的数组
// Promise.race:取最快完成的结果(无论成功失败)
const result = await Promise.race([
fetch('/api/data'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 5000)
)
])实战一:Promise.race 实现请求超时
javascript
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`请求超时: ${url} (${timeout}ms)`))
}, timeout)
})
])
}
// 使用
try {
const response = await fetchWithTimeout('/api/slow-endpoint', {}, 3000)
const data = await response.json()
} catch (err) {
console.error(err.message) // "请求超时: /api/slow-endpoint (3000ms)"
}但上面的实现有个问题:即使超时了,原来的 fetch 请求仍然在后台继续执行(只是我们不再等它了)。更好的做法是配合 AbortController(2019 年大部分现代浏览器已支持):
javascript
function fetchWithAbort(url, options = {}, timeout = 5000) {
const controller = new AbortController()
const signal = controller.signal
const timeoutId = setTimeout(() => controller.abort(), timeout)
return fetch(url, { ...options, signal })
.then(response => {
clearTimeout(timeoutId)
return response
})
.catch(err => {
clearTimeout(timeoutId)
if (err.name === 'AbortError') {
throw new Error(`请求超时: ${url}`)
}
throw err
})
}实战二:Promise.allSettled(TC39 Stage 3)
Promise.all 有一个问题:任何一个 Promise 失败,整个 all 就失败了,无法获取其他 Promise 的结果。Promise.allSettled 解决了这个问题——它等待所有 Promise 完成(无论成功失败),然后返回每个 Promise 的状态和结果。
javascript
// 提案阶段,2019 年还没有原生支持,需要 polyfill
// Promise.allSettled polyfill
if (!Promise.allSettled) {
Promise.allSettled = function(promises) {
return Promise.all(
promises.map(p =>
Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason })
)
)
)
}
}
// 使用
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/products').then(r => r.json())
])
// results:
// [
// { status: 'fulfilled', value: [...] },
// { status: 'fulfilled', value: [...] },
// { status: 'rejected', reason: Error('...') }
// ]
// 分离成功和失败
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
const failures = results
.filter(r => r.status === 'rejected')
.map(r => r.reason)实际应用场景:批量删除用户,部分可能因为权限不足而失败,但不希望一个失败影响其他操作:
javascript
async function batchDeleteUsers(userIds) {
const results = await Promise.allSettled(
userIds.map(id => deleteUser(id))
)
const deleted = []
const failed = []
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
deleted.push(userIds[index])
} else {
failed.push({ id: userIds[index], error: result.reason.message })
}
})
return { deleted, failed }
}实战三:并发控制
同时发 100 个请求?服务器会扛不住。需要控制并发数量:
javascript
class ConcurrencyPool {
constructor(concurrency = 6) {
this.concurrency = concurrency
this.running = 0
this.queue = []
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject })
this._run()
})
}
_run() {
while (this.running < this.concurrency && this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift()
this.running++
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--
this._run()
})
}
}
}
// 使用
const pool = new ConcurrencyPool(3) // 最多同时 3 个请求
const urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`)
const results = await Promise.all(
urls.map(url =>
pool.add(() => fetch(url).then(r => r.json()))
)
)更简洁的实现(不需要类):
javascript
async function parallelLimit(tasks, concurrency) {
const results = []
const executing = new Set()
for (const [index, task] of tasks.entries()) {
const promise = Promise.resolve().then(() => task())
results[index] = promise
executing.add(promise)
const cleanup = () => executing.delete(promise)
promise.then(cleanup, cleanup)
if (executing.size >= concurrency) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
// 使用
const results = await parallelLimit(
urls.map(url => () => fetch(url).then(r => r.json())),
3
)实战四:串行执行
有些场景必须串行——比如数据库迁移、文件顺序处理等:
javascript
// 方式一:用 reduce
async function serial(tasks) {
return tasks.reduce(
(promise, task) => promise.then(result => task().then(r => [...result, r])),
Promise.resolve([])
)
}
// 方式二:用 for...of + await(更直观)
async function serialFor(tasks) {
const results = []
for (const task of tasks) {
results.push(await task())
}
return results
}
// 使用
const urls = ['/api/step1', '/api/step2', '/api/step3']
const results = await serialFor(
urls.map(url => () => fetch(url).then(r => r.json()))
)实战五:手动实现 Promise.all
理解原理比会用更重要:
javascript
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'))
}
const results = []
let completedCount = 0
const len = promises.length
if (len === 0) {
return resolve(results)
}
promises.forEach((promise, index) => {
// 确保非 Promise 值也能处理
Promise.resolve(promise)
.then(value => {
results[index] = value
completedCount++
if (completedCount === len) {
resolve(results)
}
})
.catch(reject) // 任何一个失败,立即 reject
})
})
}
// 测试
const p1 = Promise.resolve(1)
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100))
const p3 = Promise.resolve(3)
const result = await promiseAll([p1, p2, p3])
console.log(result) // [1, 2, 3]实战六:retry 重试机制
网络请求经常需要失败重试:
javascript
async function retry(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn()
} catch (err) {
if (i === retries - 1) throw err
// 指数退避:每次重试等待时间翻倍
const waitTime = delay * Math.pow(2, i)
console.log(`第 ${i + 1} 次重试,等待 ${waitTime}ms`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
// 使用
const data = await retry(
() => fetch('/api/unstable').then(r => r.json()),
3,
1000
)踩坑记录
坑 1:忘记 return Promise
javascript
// 错误:.then 回调中没有 return
getUser(id)
.then(user => {
getOrders(user.id) // 没有 return!
})
.then(orders => {
console.log(orders) // undefined
})
// 正确
getUser(id)
.then(user => {
return getOrders(user.id)
})
.then(orders => {
console.log(orders)
})坑 2:在 forEach 中使用 async/await
javascript
// 错误:forEach 不会等待异步回调
const ids = [1, 2, 3]
ids.forEach(async (id) => {
await deleteUser(id) // 并发执行,不是串行
})
console.log('删除完成') // 不会等待上面的删除操作
// 正确串行:
for (const id of ids) {
await deleteUser(id)
}
console.log('删除完成') // 会等待
// 正确并发(需要等待全部完成):
await Promise.all(ids.map(id => deleteUser(id)))坑 3:Promise 构造函数中的同步错误
javascript
// 同步错误会被 Promise 捕获
new Promise((resolve) => {
JSON.parse('invalid json') // 同步抛出错误
resolve()
}).catch(err => {
console.error(err) // SyntaxError: Unexpected token
// 这里能捕获到
})坑 4:微任务执行时机
javascript
console.log('start')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('end')
// 输出顺序: start → end → promise → timeout
// Promise.then 是微任务,优先级高于 setTimeout(宏任务)小结
Promise.race适合做超时控制,配合AbortController可以真正取消请求Promise.allSettled适合批量操作中允许部分失败的场景(需 polyfill)- 并发控制是实际项目中的高频需求,用队列 + 计数器实现
- 串行执行用
for...of+await最直观 - 手写
Promise.all帮助理解原理:收集所有结果,计数器判断全部完成 - 注意
forEach+ async 的坑,它不会等待异步回调完成