前端开发
深入理解 JavaScript 异步编程
从回调到 Promise 再到 async/await,彻底搞懂 JavaScript 异步编程的演进与最佳实践。
站站长
15 分钟阅读
深入理解 JavaScript 异步编程
JavaScript 是单线程语言,但通过事件循环机制实现了异步编程。本文将全面讲解异步编程的演进历程。
1. 为什么需要异步
JavaScript 在浏览器中运行在一个单线程上。如果所有操作都是同步的,一个耗时操作就会阻塞整个页面:
// 同步:阻塞
const data = syncFetch('/api/users') // 网络请求耗时 2 秒
console.log('这行代码要等 2 秒后才执行')
// 异步:非阻塞
asyncFetch('/api/users').then(data => {
console.log('请求完成', data)
})
console.log('这行代码立即执行')
2. 回调函数(Callback)
最早的异步方案:
function fetchData(url, callback) {
setTimeout(() => {
callback('数据')
}, 1000)
}
fetchData('/api', (data) => {
console.log(data)
})
回调地狱
// ❌ 回调地狱
fetchUser(id, (user) => {
fetchOrders(user.id, (orders) => {
fetchOrderDetail(orders[0].id, (detail) => {
fetchProducts(detail.productId, (products) => {
console.log(products)
})
})
})
})
3. Promise
Promise 解决了回调地狱问题,提供了链式调用:
fetchUser(id)
.then(user => fetchOrders(user.id))
.then(orders => fetchOrderDetail(orders[0].id))
.then(detail => fetchProducts(detail.productId))
.then(products => console.log(products))
.catch(err => console.error('出错了:', err))
Promise 的三种状态
- pending — 初始状态,既不是成功也不是失败
- fulfilled — 操作成功
- rejected — 操作失败
常用 API
// 并行执行所有 Promise
Promise.all([p1, p2, p3])
.then(results => console.log(results))
// 返回第一个完成的(无论成功或失败)
Promise.race([p1, p2])
.then(result => console.log(result))
// 等待所有完成(无论成功或失败)
Promise.allSettled([p1, p2, p3])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
console.log('成功:', r.value)
} else {
console.log('失败:', r.reason)
}
})
})
4. async/await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
async function getData() {
try {
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)
const detail = await fetchOrderDetail(orders[0].id)
const products = await fetchProducts(detail.productId)
console.log(products)
} catch (err) {
console.error('出错了:', err)
}
}
并行请求优化
// ❌ 串行:总耗时 = 请求A + 请求B
const a = await fetchA() // 2秒
const b = await fetchB() // 3秒
// 总耗时:5秒
// ✅ 并行:总耗时 = max(请求A, 请求B)
const [a, b] = await Promise.all([fetchA(), fetchB()])
// 总耗时:3秒
5. 事件循环(Event Loop)
理解事件循环是掌握异步编程的关键:
console.log('1. 同步代码')
setTimeout(() => {
console.log('4. 宏任务')
}, 0)
Promise.resolve().then(() => {
console.log('3. 微任务')
})
console.log('2. 同步代码')
// 输出顺序:1 → 2 → 3 → 4
执行优先级
- 同步代码(调用栈)
- 微任务(Promise.then、queueMicrotask、MutationObserver)
- 宏任务(setTimeout、setInterval、I/O、UI 渲染)
6. 实际应用
请求重试
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
if (i === retries - 1) throw err
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}
请求取消
const controller = new AbortController()
async function fetchData() {
try {
const res = await fetch('/api/data', {
signal: controller.signal
})
return await res.json()
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求被取消')
}
}
}
// 取消请求
controller.abort()
并发控制
async function asyncPool(limit, items, iteratorFn) {
const results = []
const executing = new Set()
for (const item of items) {
const p = Promise.resolve().then(() => iteratorFn(item))
results.push(p)
executing.add(p)
p.finally(() => executing.delete(p))
if (executing.size >= limit) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
// 限制并发为 3
await asyncPool(3, urls, url => fetch(url))
总结
| 方案 | 优点 | 缺点 |
|---|---|---|
| 回调 | 简单 | 回调地狱、错误处理困难 |
| Promise | 链式调用、错误传递 | 仍有嵌套、不够直观 |
| async/await | 语法清晰、像同步代码 | 需要 Promise 基础 |
异步编程是 JavaScript 的核心能力之一。理解事件循环、熟练使用 async/await,你就能写出优雅的异步代码!
📖 相关阅读
⚡
前端开发
2024-10-20
Web 性能优化实战:从首屏加载到运行时流畅
系统性地讲解 Web 性能优化的各个方面,包括加载性能、渲染性能、运行时性能的优化策略。
#性能优化#前端#Web
14 分钟阅读
阅读全文 →
🔗
前端开发
2024-12-15
React 开发最佳实践指南(2024 版)
从组件设计到状态管理,从性能优化到代码规范,全面总结 React 开发中的最佳实践。
#React#前端#最佳实践
12 分钟阅读
阅读全文 →
📘
前端开发
2024-09-01
TypeScript 泛型完全指南
从基础到高级,彻底搞懂 TypeScript 泛型,写出更灵活、更类型安全的代码。
#TypeScript#前端#类型系统
12 分钟阅读
阅读全文 →