1. 为什么我们需要Promise的多种方法?
在JavaScript异步编程的世界里,Promise早已成为处理异步操作的标准方式。但很多开发者在使用Promise时,往往只停留在.then()和.catch()的基本用法上,对Promise提供的丰富方法集缺乏系统认识。这就像只学会了开车,却不知道车上还有空调、定速巡航等实用功能一样可惜。
Promise的静态方法和实例方法各司其职:
- 静态方法(如
Promise.all())通常用于处理多个Promise之间的关系 - 实例方法(如
.then())则用于处理单个Promise的生命周期
最近我在排查一个前端性能问题时发现,开发者因为不熟悉Promise.allSettled(),而用复杂的逻辑手动实现了类似功能,不仅代码冗余,还引入了潜在的竞态条件。这让我意识到系统梳理Promise方法的重要性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Promise实例方法深度解析
2.1 then():Promise链式调用的核心
.then()是Promise最基础也最重要的方法,它接收两个参数:
javascript复制promise.then(
onFulfilled?: (value: any) => any,
onRejected?: (reason: any) => any
)
实际开发中常见的误区是只传第一个参数:
javascript复制fetch('/api').then(handleSuccess) // 错误:没有错误处理
更健壮的写法应该是:
javascript复制fetch('/api')
.then(handleSuccess)
.catch(handleError) // 或者使用then的第二个参数
提示:
.then()会返回一个新的Promise,这是链式调用的基础。新Promise的状态由回调函数的返回值决定:
- 返回普通值 → fulfilled
- 抛出错误 → rejected
- 返回Promise → 继承该Promise状态
2.2 catch():不只是错误捕获
.catch()本质上是.then(null, onRejected)的语法糖,但有个关键区别:.catch()会捕获链上所有未被处理的错误。
典型错误用法:
javascript复制fetch('/api')
.then(res => {
return processData(res) // 如果这里出错
}, handleError) // 这个处理函数不会捕获上面的错误
正确做法:
javascript复制fetch('/api')
.then(res => processData(res))
.catch(handleError) // 能捕获整个链中的错误
我在实际项目中发现,合理使用.catch()可以避免"Uncaught (in promise)"错误,这是控制台常见的警告之一。
2.3 finally():无论成败都要执行的逻辑
ES2018引入的.finally()特别适合处理无论成功失败都需要执行的逻辑,比如隐藏loading状态:
javascript复制showLoading()
fetch('/api')
.then(handleSuccess)
.catch(handleError)
.finally(hideLoading) // 无论成功失败都会执行
需要注意的是:
.finally()回调不接收任何参数- 它返回的Promise会继承原Promise的状态
- 如果回调抛出错误或返回rejected Promise,会覆盖原状态
3. Promise静态方法应用场景
3.1 Promise.all():并行执行多个异步操作
Promise.all()在需要等待多个异步操作全部完成时非常有用:
javascript复制const [user, posts] = await Promise.all([
fetch('/user'),
fetch('/posts')
])
关键特性:
- 接收一个Promise数组
- 所有Promise都resolve时才会resolve
- 任何一个Promise reject会立即reject
- 结果数组顺序与输入一致
我在优化页面加载速度时常用这个模式,可以并行请求多个接口,而不是串行等待。
3.2 Promise.allSettled():不因个别失败而中断
ES2020引入的Promise.allSettled()解决了Promise.all()的"一票否决"问题:
javascript复制const results = await Promise.allSettled([
fetch('/api1'), // 成功
fetch('/api2') // 失败
])
// results:
// [
// {status: "fulfilled", value: response1},
// {status: "rejected", reason: error}
// ]
这在需要获取多个数据源,且不希望因个别接口失败而中断整个流程的场景下非常有用。
3.3 Promise.race():竞速场景应用
Promise.race()返回第一个settled的Promise的结果:
javascript复制function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
}
await Promise.race([
fetch('/api'),
timeout(5000) // 5秒超时
])
常见应用场景:
- 请求超时处理
- 多服务器请求,取最快响应
- 用户操作与自动操作的竞速
3.4 Promise.any():取第一个成功结果
ES2021新增的Promise.any()与race()不同,它等待第一个fulfilled的Promise:
javascript复制const firstSuccess = await Promise.any([
fetch('backup1.com'),
fetch('backup2.com') // 假设这个先成功
])
如果所有Promise都rejected,会抛出AggregateError包含所有错误。
4. 高级应用与性能优化
4.1 避免Promise地狱
虽然Promise解决了回调地狱,但不恰当的使用仍会导致嵌套问题:
javascript复制// 不好的写法
fetch('/user').then(user => {
fetch(`/posts/${user.id}`).then(posts => {
fetch(`/comments/${posts[0].id}`).then(comments => {
// ...
})
})
})
改进方案:
javascript复制fetch('/user')
.then(user => fetch(`/posts/${user.id}`))
.then(posts => fetch(`/comments/${posts[0].id}`))
.then(comments => {
// ...
})
4.2 合理控制并发数
当需要处理大量异步操作时,直接使用Promise.all可能导致资源耗尽:
javascript复制// 一次性发起1000个请求
const promises = Array(1000).fill().map(() => fetch('/api'))
await Promise.all(promises) // 可能导致浏览器崩溃
解决方案是实现并发控制:
javascript复制async function parallelWithLimit(tasks, limit) {
const results = []
const executing = new Set()
for (const task of tasks) {
const p = task().then(res => {
executing.delete(p)
return res
})
executing.add(p)
results.push(p)
if (executing.size >= limit) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
4.3 内存泄漏预防
Promise相关的内存泄漏常常被忽视:
javascript复制function startPolling() {
return fetch('/data').then(data => {
process(data)
setTimeout(startPolling, 5000) // 循环引用
})
}
解决方案是使用AbortController:
javascript复制const controller = new AbortController()
function startPolling() {
return fetch('/data', {signal: controller.signal})
.then(data => {
process(data)
setTimeout(startPolling, 5000)
})
.catch(e => {
if (e.name !== 'AbortError') throw e
})
}
// 需要停止时
controller.abort()
5. 错误处理最佳实践
5.1 全局Promise错误捕获
未处理的Promise rejection会导致警告:
code复制Uncaught (in promise) Error: ...
浏览器环境可以通过以下方式捕获:
javascript复制window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason)
event.preventDefault() // 阻止默认错误输出
})
Node.js环境:
javascript复制process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason)
})
5.2 错误传播与转换
合理的错误处理策略应该:
- 在适当的层级处理错误
- 转换错误类型以便上层处理
- 保留原始错误堆栈
javascript复制class ApiError extends Error {
constructor(message, originalError) {
super(message)
this.originalError = originalError
this.stack = originalError.stack
}
}
fetch('/api')
.then(res => {
if (!res.ok) {
throw new ApiError(`HTTP error ${res.status}`, res)
}
return res.json()
})
.catch(err => {
if (err instanceof ApiError) {
// 处理已知错误
} else {
// 处理意外错误
}
})
5.3 重试机制实现
对于临时性错误,实现自动重试能提升用户体验:
javascript复制function fetchWithRetry(url, options = {}, retries = 3) {
return fetch(url, options).catch(err => {
if (retries <= 0 || !isRetriableError(err)) throw err
return new Promise(resolve =>
setTimeout(
() => resolve(fetchWithRetry(url, options, retries - 1)),
getRetryDelay(retries)
)
)
})
}
function isRetriableError(err) {
// 网络错误或5xx错误可重试
return err instanceof TypeError ||
(err.response && err.response.status >= 500)
}
function getRetryDelay(retryCount) {
return Math.pow(2, retryCount) * 1000 // 指数退避
}
