为什么 JavaScript 需要异步?
JavaScript 是单线程语言,同一时间只能执行一个任务。如果所有操作都是同步的,当遇到网络请求、文件读取等耗时操作时,整个程序会被阻塞。异步编程就是为了解决这个问题——让耗时操作在后台执行,不阻塞主线程。
从回调函数说起
最初的异步方案是回调函数(Callback),但它容易导致"回调地狱":
// 回调地狱 —— 代码横向发展,难以维护
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getShipping(detail.shippingId, function(shipping) {
console.log(shipping.status)
// 更多嵌套...
})
})
})
})
回调函数的问题:嵌套层级深、错误处理分散、代码可读性差。
Promise —— 异步编程的革命
Promise 是 ES6 引入的异步解决方案,它用链式调用取代了嵌套回调。
Promise 的三种状态
| 状态 | 说明 | 是否可变 |
|---|---|---|
| pending | 初始状态,既未完成也未拒绝 | → fulfilled 或 rejected |
| fulfilled | 操作成功完成 | 不可变 |
| rejected | 操作失败 | 不可变 |
创建 Promise
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`请求失败: ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('网络错误'))
xhr.send()
})
}
链式调用
// 链式调用 —— 扁平化,清晰易读
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => getShipping(detail.shippingId))
.then(shipping => console.log(shipping.status))
.catch(error => console.error('出错了:', error))
实用的 Promise 静态方法
// Promise.all —— 并行执行,全部成功才算成功
const [user, config, theme] = await Promise.all([
fetchUser(),
fetchConfig(),
fetchTheme()
])
// Promise.allSettled —— 等待全部完成,不管成功失败
const results = await Promise.allSettled([
fetch('/api/fast'), // 可能成功
fetch('/api/slow'), // 可能超时
fetch('/api/unstable') // 可能失败
])
results.forEach(r => {
if (r.status === 'fulfilled') console.log('成功:', r.value)
else console.log('失败:', r.reason)
})
// Promise.race —— 返回最先完成的结果
const result = await Promise.race([
fetch('/api/data'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
)
])
// Promise.any —— 返回最先成功的结果
const fastest = await Promise.any([
fetch('/mirror1/api'),
fetch('/mirror2/api'),
fetch('/mirror3/api')
])
Async/Await —— 优雅的异步语法
Async/Await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码一样自然。
// async 函数总是返回 Promise
async function getUserShipping(userId) {
const user = await getUser(userId)
const orders = await getOrders(user.id)
const detail = await getOrderDetail(orders[0].id)
const shipping = await getShipping(detail.shippingId)
return shipping.status
}
// 使用 try/catch 处理错误
async function safeFetchData(url) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('请求失败:', error.message)
return null
} finally {
console.log('请求完成') // 无论成功失败都会执行
}
}
并行执行优化
// ❌ 错误:串行执行,总耗时 = t1 + t2 + t3
async function slow() {
const a = await fetch('/api/a') // 2秒
const b = await fetch('/api/b') // 3秒
const c = await fetch('/api/c') // 1秒
// 总耗时 6 秒
}
// ✅ 正确:并行执行,总耗时 = max(t1, t2, t3)
async function fast() {
const [a, b, c] = await Promise.all([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c')
])
// 总耗时 3 秒(取最长的)
}
深入事件循环
理解异步的关键在于理解事件循环(Event Loop)。JavaScript 的执行模型基于以下概念:
调用栈、任务队列与微任务
console.log('1. 同步') // 调用栈
setTimeout(() => {
console.log('4. 宏任务') // 宏任务队列(Task Queue)
}, 0)
Promise.resolve().then(() => {
console.log('3. 微任务') // 微任务队列(Microtask Queue)
})
console.log('2. 同步')
// 输出顺序: 1 → 2 → 3 → 4
事件循环的执行顺序:
- 执行调用栈中的同步代码
- 调用栈清空后,执行所有微任务(Promise.then、queueMicrotask)
- 取一个宏任务执行(setTimeout、setInterval、I/O)
- 重复步骤 2-3
// 经典面试题:输出顺序
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end') // 微任务
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout') // 宏任务
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2') // 微任务
})
console.log('script end')
// 输出:
// script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout
错误处理最佳实践
// 工具函数:包装 async 函数,返回 [error, data]
async function to(promise) {
try {
const data = await promise
return [null, data]
} catch (error) {
return [error, null]
}
}
// 使用方式
async function handleRequest() {
const [err, user] = await to(fetchUser(id))
if (err) {
showError('用户加载失败')
return
}
const [err2, orders] = await to(fetchOrders(user.id))
if (err2) {
showError('订单加载失败')
return
}
renderOrders(orders)
}
总结
- 回调函数:最基础的异步方式,容易产生回调地狱
- Promise:链式调用,统一错误处理,提供丰富的静态方法
- Async/Await:最优雅的写法,代码可读性最佳
- 事件循环:理解微任务和宏任务的区别,是排查异步 bug 的关键
在实际开发中,推荐使用 Async/Await 处理异步逻辑,配合 Promise.all 实现并行请求,用 try/catch 进行错误处理。理解事件循环机制,能帮助你写出更高效、更可靠的异步代码。