十年前我刚接触JavaScript时,回调地狱(callback hell)是每个前端开发者的噩梦。金字塔式的代码缩进不仅难以阅读,错误处理更是令人抓狂。直到ES6引入Promise,才让我们看到了曙光。而async/await作为ES2017的标准,可以说是异步编程的终极解决方案。
这两种技术本质上都是为解决JavaScript单线程模型下的异步操作而生。Promise通过链式调用解决了回调嵌套问题,而async/await则让异步代码拥有了同步代码的书写体验。但很多开发者,尤其是初学者,常常混淆二者的使用场景。
Promise本质上是一个状态机,它有三种可能的状态:
一旦状态从Pending转变为其他两种状态之一,就不可再改变。这种不可变性(immutability)是Promise可靠性的基础。
javascript复制const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const random = Math.random()
if (random > 0.5) {
resolve('成功结果')
} else {
reject(new Error('失败原因'))
}
}, 1000)
})
Promise真正的威力在于其链式调用能力。每个then()方法都会返回一个新的Promise,这使得我们可以将多个异步操作串联起来:
javascript复制fetch('/api/data')
.then(response => response.json())
.then(data => processData(data))
.catch(error => handleError(error))
.finally(() => cleanUp())
重要提示:永远不要忘记在Promise链的末尾添加catch(),否则未捕获的rejection可能会导致难以调试的问题。
Promise类提供了几个实用的静态方法:
javascript复制// 并行请求多个接口
const [user, posts] = await Promise.all([
fetch('/user'),
fetch('/posts')
])
async/await实际上是基于Promise的语法糖,它让异步代码看起来像同步代码:
javascript复制async function fetchData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
return processData(data)
} catch (error) {
handleError(error)
} finally {
cleanUp()
}
}
很多人误解await会"阻塞"代码执行。实际上,await只是暂停当前async函数的执行,将控制权交还给事件循环。JavaScript线程本身并没有被阻塞。
javascript复制console.log('开始')
async function demo() {
console.log('函数内开始')
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('await之后')
}
demo()
console.log('结束')
// 输出顺序:
// 开始
// 函数内开始
// 结束
// (1秒后)
// await之后
async函数总是返回一个Promise。即使你返回一个非Promise值,它也会被自动包装成Promise:
javascript复制async function foo() {
return 42
}
// 等价于
function foo() {
return Promise.resolve(42)
}
Promise使用.catch()方法处理错误,而async/await使用try/catch:
javascript复制// Promise方式
fetchData()
.then(handleData)
.catch(handleError)
// async/await方式
try {
const data = await fetchData()
handleData(data)
} catch (error) {
handleError(error)
}
经验之谈:在复杂的异步流程中,try/catch通常比链式.catch()更易于维护。
对于简单的线性异步操作,async/await明显更清晰:
javascript复制// Promise链
function getProcessedData(url) {
return fetch(url)
.then(response => response.json())
.then(data => processData(data))
.catch(error => handleError(error))
}
// async/await
async function getProcessedData(url) {
try {
const response = await fetch(url)
const data = await response.json()
return processData(data)
} catch (error) {
handleError(error)
}
}
但对于需要并行处理多个异步操作的场景,Promise的组合方法更简洁:
javascript复制// 更好的方式
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders()
])
从性能角度看,async/await和Promise几乎没有区别,因为它们本质上是同一种机制的不同表现形式。但在某些极端情况下:
javascript复制// 不推荐 - 每个await都会创建新的Promise
async function slow() {
const a = await getA() // 创建Promise
const b = await getB() // 创建Promise
return a + b
}
// 推荐 - 并行请求
async function fast() {
const [a, b] = await Promise.all([getA(), getB()])
return a + b
}
对于需要缓存异步结果的场景,可以结合闭包实现记忆化:
javascript复制function memoizedFetch(url) {
const cache = new Map()
return async function() {
if (cache.has(url)) {
return cache.get(url)
}
const response = await fetch(url)
const data = await response.json()
cache.set(url, data)
return data
}
}
忘记await:这会导致async函数继续执行而不等待异步操作完成
javascript复制async function demo() {
const data = fetch('/api') // 缺少await!
console.log(data) // 输出Promise对象而非实际数据
}
不必要的串行await:
javascript复制// 低效写法
async function slow() {
const a = await getA()
const b = await getB() // 等待getA完成才开始
return a + b
}
// 高效写法
async function fast() {
const [a, b] = await Promise.all([getA(), getB()]) // 并行执行
return a + b
}
在循环中错误使用await:
javascript复制// 顺序执行,效率低
for (const url of urls) {
await fetch(url)
}
// 并行执行
await Promise.all(urls.map(url => fetch(url)))
JavaScript原生不支持取消Promise,但可以通过AbortController实现类似效果:
javascript复制async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, { signal: controller.signal })
clearTimeout(timeoutId)
return response.json()
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求超时')
}
throw error
}
}
在实际项目中,最佳实践是根据场景灵活选择:
javascript复制// 顶层使用async/await
async function main() {
try {
// 并行请求使用Promise.all
const [user, products] = await Promise.all([
fetchUser(),
fetchProducts()
])
// 顺序处理使用await
for (const product of products) {
await processProduct(product)
}
// 复杂逻辑可以封装为Promise链
return generateReport(user, products)
.then(uploadReport)
.then(notifyUser)
} catch (error) {
handleError(error)
}
}
全局未处理的Promise rejection:
javascript复制process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的rejection:', reason)
})
使用高阶函数封装错误处理:
javascript复制function withErrorHandling(fn) {
return async function(...args) {
try {
return await fn(...args)
} catch (error) {
logError(error)
throw error // 或者返回默认值
}
}
}
const safeFetch = withErrorHandling(fetch)
给Promise添加标签:
javascript复制function createLabeledPromise(label, promise) {
promise.label = label
return promise
}
const p = createLabeledPromise('用户数据', fetchUser())
使用async堆栈追踪:
javascript复制// 在Node.js中启用async堆栈追踪
NODE_OPTIONS=--async-stack-traces node app.js
可视化Promise执行流程:
javascript复制function tracePromise(promise, name) {
promise.then(
v => console.log(`${name} resolved with`, v),
e => console.log(`${name} rejected with`, e)
)
return promise
}
tracePromise(fetchData(), '数据请求')
在大型项目中,我通常会创建一个自定义的Promise子类,集成这些调试功能:
javascript复制class DebugPromise extends Promise {
constructor(executor, label = '') {
super(executor)
this.label = label
}
then(onFulfilled, onRejected) {
console.log(`Promise ${this.label} then called`)
return super.then(onFulfilled, onRejected)
}
}
const p = new DebugPromise(resolve => {
setTimeout(resolve, 1000)
}, '延时Promise')