JavaScript 的异步模型是前端开发的核心能力之一。本文从事件循环出发,梳理 callback、Promise 到 async/await 的演进路线。

1. 为什么 JavaScript 需要异步

JavaScript 是单线程语言,这意味着同一时间只能执行一段代码。如果耗时操作阻塞主线程,页面就会卡顿甚至失去响应。因此,网络请求、定时器、文件读写等操作必须被设计成异步执行。

2. 回调函数时代

最早的异步写法是回调函数:

1
2
3
setTimeout(() => {
console.log('1 秒后执行')
}, 1000)

嵌套一多,就会出现「回调地狱」(Callback Hell),代码横向扩展,难以维护:

1
2
3
4
5
6
7
getData(url, (data) => {
processData(data, (result) => {
saveResult(result, (msg) => {
console.log(msg)
})
})
})

3. Promise:链式调用的优雅

ES6 引入的 Promise 将异步操作封装为对象,通过 .then().catch() 进行链式处理:

1
2
3
4
fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err))

Promise 解决了回调地狱的问题,但在复杂场景下,链式调用依然会显得冗长。

4. async/await:写异步像写同步

ES2017 推出的 async/await 是 Promise 的语法糖,让异步代码的可读性大幅提升:

1
2
3
4
5
6
7
8
9
async function getUser() {
try {
const res = await fetch('/api/user')
const data = await res.json()
return data
} catch (err) {
console.error(err)
}
}

常见误区

  • await 只能在 async 函数内部使用(顶层 await 在 ES2022 的模块中已支持)。
  • async 函数默认返回一个 Promise,即使你没有显式 return。
  • 多个无依赖的异步请求不要顺序 await,应该用 Promise.all 并行执行:
1
2
3
4
const [users, posts] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts')
])

5. 事件循环:理解执行顺序

浏览器中,JavaScript 引擎通过调用栈(Call Stack)执行同步代码,异步任务则被放入任务队列(Task Queue)。事件循环(Event Loop)的职责就是:

  1. 先清空调用栈中的同步代码。
  2. 检查微任务队列(Microtask Queue,如 Promise.then),全部执行完毕。
  3. 取出一个宏任务(Macrotask,如 setTimeout、setInterval)执行。
  4. 重复步骤 2~3。

理解这一机制,才能正确处理复杂的异步执行顺序问题。

总结

从回调到 Promise,再到 async/await,JavaScript 的异步编程变得越来越优雅。但在实际开发中,仍需掌握事件循环原理,才能避免踩入执行顺序和性能优化的坑。