JavaScript 异步控制流:从 Promise 到 async/await 的工程实践
JavaScript 的异步模型从回调地狱走到 async/await,已经相当成熟。但很多开发者还是会踩坑:错误被吞掉、并发控制不当、竞态条件频发。
一、Promise 的基本纪律
1.1 永远处理 reject
// 错误示范
fetch('/api/data').then(res => res.json())
// 正确示范
fetch('/api/data')
.then(res => res.json())
.catch(err => {
console.error('请求失败:', err)
showErrorMessage(err)
})
1.2 不要嵌套 Promise
// 回调地狱的 Promise 版本 - 不要这样写
getUser(id).then(user => {
getOrders(user.id).then(orders => {
getOrderDetail(orders[0].id).then(detail => {
// ...
})
})
})
// 应该这样写
const user = await getUser(id)
const orders = await getOrders(user.id)
const detail = await getOrderDetail(orders[0].id)
二、并发控制
2.1 Promise.all - 全部成功才成功
const [user, orders, products] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchProducts()
])
一个失败,全部失败。适合有强依赖关系的场景。
2.2 Promise.allSettled - 等全部完成
const results = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c')
])
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${i} 成功:`, result.value)
} else {
console.log(`请求 ${i} 失败:`, result.reason)
}
})
适合"部分失败不影响整体"的场景。
2.3 Promise.race - 谁先完成用谁
const response = await Promise.race([
fetch('/api/fast-server'),
fetch('/api/backup-server'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
)
])
三、错误处理的工程化
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求被取消')
}
throw error
}
}
关键原则:不要在 async 函数里用 try-catch 吞掉错误后不做任何处理。
四、竞态条件处理
用户快速输入搜索时,旧的请求可能比新的请求晚返回:
let latestRequestId = 0
async function search(query) {
const requestId = ++latestRequestId
const results = await fetch(`/api/search?q=${query}`)
// 如果已经有更新的请求,丢弃这次结果
if (requestId !== latestRequestId) return
displayResults(results)
}
或者用 AbortController:
let controller = null
async function search(query) {
if (controller) controller.abort()
controller = new AbortController()
const results = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
displayResults(results)
}
五、重试机制
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fetch(url, options)
} catch (err) {
if (i === maxRetries) throw err
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
}
}
}
指数退避(1s, 2s, 4s)是重试策略的最佳实践。
六、实用工具函数
// 限制并发数
async function parallelLimit(tasks, limit) {
const results = []
const executing = new Set()
for (const [i, task] of tasks.entries()) {
const p = task().then(r => { results[i] = r })
executing.add(p)
p.then(() => executing.delete(p))
if (executing.size >= limit) {
await Promise.race(executing)
}
}
await Promise.all(executing)
return results
}
异步编程的本质是控制不确定性。掌握这些模式,你的前端代码会更健壮、更可维护。


钱哆哆♥官方正规流量卡♥1 个月前
生死门虽繁星灿烂,但活着的人才是最重要。
钱哆哆♥官方正规流量卡♥1 个月前
《技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法》已更新:技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法 很多技术博客的正文其实不差,问题常常出在视觉层太单一。首页列表里大家都只有一张封面,点进去以后又是一大段连续文字,读者很难在几秒钟内判断这篇文章到底值不值得继续看。内容本身也许很扎实,但呈现方式没有把价值推出来。…
钱哆哆♥官方正规流量卡♥1 个月前
《技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法》已更新:技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法 很多技术博客的正文其实不差,问题常常出在视觉层太单一。首页列表里大家都只有一张封面,点进去以后又是一大段连续文字,读者很难在几秒钟内判断这篇文章到底值不值得继续看。内容本身也许很扎实,但呈现方式没有把价值推出来。…
钱哆哆♥官方正规流量卡♥1 个月前
《技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法》已更新:技术博客图文文章怎么做得不单一:封面、结构图与场景插图的组合方法 很多技术博客的正文其实不差,问题常常出在视觉层太单一。首页列表里大家都只有一张封面,点进去以后又是一大段连续文字,读者很难在几秒钟内判断这篇文章到底值不值得继续看。内容本身也许很扎实,但呈现方式没有把价值推出来。…
钱哆哆♥官方正规流量卡♥1 个月前
你和学霸的区别就是,你所有的灵光一闪,都是他的基本题型。