前面我们介绍了如何使用 Promise 解决回调地狱问题。然而,即便使用了 Promise,如果处理流程复杂,代码中仍会充斥大量的 .then() 方法,导致语义不够清晰,难以直观地反映代码的执行流程。
以下是一个使用 fetch 发起连续网络请求的例子,fetch 是浏览器原生支持的 API,它返回一个 Promise 对象。
fetch('https://www.geekbang.org')
.then((response) => {
console.log(response)
return fetch('https://www.geekbang.org/test')
})
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
尽管 Promise 让异步流程线性化了,但 .then 的链式调用仍然让代码显得繁琐,可读性有待提高。因此,ES7 引入了 async/await,这是 JavaScript 异步编程的一次重大革新。它允许我们以同步代码的风格编写异步逻辑,在不阻塞主线程的前提下访问资源,极大提升了代码的清晰度。
async function foo(){
try{
let response1 = await fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}catch(err) {
console.error(err)
}
}
foo()
通过这段代码,你会发现整个异步操作被封装在了同步的代码结构中,并且能够使用 try...catch 进行异常捕获。这完全符合人类的线性思维习惯。不过,要真正理解这种“同步风格异步代码”背后的魔法,我们需要先了解两个底层概念:生成器(Generator)与协程(Coroutine)。async/await 正是结合了 Generator 和 Promise 两种技术的语法糖。
生成器(Generator)与协程(Coroutine)原理
生成器函数是一种可以暂停执行和恢复执行的特殊函数。 在定义时,需要在 function 关键字后添加一个星号 *。
function* genDemo() {
console.log("开始执行第一段")
yield 'generator 2'
console.log("开始执行第二段")
yield 'generator 2'
console.log("开始执行第三段")
yield 'generator 2'
console.log("执行结束")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
执行上面代码,你会发现 genDemo 函数并非一次性执行完毕,而是与全局代码交替执行。这正是生成器的核心特性:
- 在生成器函数内部执行时,遇到
yield 关键字,引擎会暂停当前函数执行,并将 yield 后面的值返回给外部。
- 外部可以通过调用生成器对象的
.next() 方法,恢复该函数的执行。
那么,函数的“暂停”与“恢复”是如何实现的呢?这就要引出协程的概念。
协程是一种比线程更加轻量级的执行单元。你可以将它理解为跑在线程上的任务。一个线程上可以同时存在多个协程,但在任意时刻,只能执行其中一个。如果当前正在执行协程 A,要启动协程 B,就需要 A 暂停并将线程控制权交给 B;反之亦然。通常,我们称启动方为父协程。
协程完全由程序控制(在用户态执行),而非操作系统内核管理。这避免了线程切换的开销,性能优势显著。在前端框架/工程化开发中,理解这些底层机制对性能优化很有帮助。

从上图及代码执行中,我们可以总结出协程的几点关键规则:
- 调用生成器函数
genDemo 会创建一个协程 gen,但创建后它不会立即执行。
- 必须通过调用
gen.next() 来启动或恢复 gen 协程的执行。
- 协程执行中遇到
yield 会暂停,并将信息返回给父协程。
- 协程执行中遇到
return,JavaScript 引擎会结束当前协程,并将值返回给父协程。
特别需要注意的是:
gen 协程与父协程是交替执行的,并非并发。
- 当
yield 或 gen.next() 被调用时,JavaScript 引擎会保存当前协程的调用栈信息,并恢复另一个协程的调用栈信息,从而实现执行上下文的切换。

实际上,生成器就是 JavaScript 中协程的一种实现方式。现在,我们可以用生成器配合 Promise 来改写最初的异步请求示例。
//foo函数
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})
这段代码的工作流程如下:
- 执行
let gen = foo(),创建生成器协程 gen。
- 父协程执行
gen.next(),将控制权交给 gen 协程。
gen 协程执行 fetch 创建 Promise 对象 response1,然后通过 yield 暂停自身,并将 response1 返回给父协程。
- 父协程恢复,调用
response1.then() 注册回调,等待请求完成。
- 请求完成后,
then 的回调被触发,在其中再次调用 gen.next(),将控制权和结果交还给 gen 协程,继续执行下一个请求。
通常,我们会将执行生成器的逻辑封装成一个函数,称为执行器(如著名的 co 库),以简化调用。
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());
深入解析 async/await 机制
虽然“生成器 + Promise + 执行器”的模式已经可行,但为了追求更简洁直观的代码,ES7 正式推出了 async/await 语法。它的底层仍然是 Promise 和生成器(协程),可以看作是对这两者的高级封装。
1. async 函数
根据 MDN 定义,async 函数是一个通过异步执行并隐式返回 Promise 作为结果的函数。
async function foo() {
return 2
}
console.log(foo()) // 输出: Promise {<fulfilled>: 2}
2. await 表达式
await 的行为是理解 async/await 的关键。让我们分析一个简单例子:
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
// 执行结果:
// 0
// 1
// 3
// 100
// 2
让我们站在协程的视角,结合下图来梳理整个执行流程:

- 首先打印
0。
- 执行
foo()。由于 foo 被标记为 async,引擎会保存当前调用栈信息。接着执行 foo 内部的 console.log(1),打印 1。
- 执行
await 100。这一步隐含了以下操作:
- 引擎会默认创建一个 Promise 对象(
let promise_ = new Promise((resolve) => resolve(100)))。
- 创建 Promise 时,
resolve(100) 的任务会被提交到微任务队列。
foo 协程暂停执行,控制权交还给父协程,同时将 promise_ 对象返回。
- 父协程获得控制权后,会调用
promise_.then() 来监听 Promise 状态变化。
- 父协程继续执行,打印
3。父协程执行完毕后,进入微任务检查点,开始执行微任务队列。
- 微任务队列中的
resolve(100) 任务被执行,这会触发 promise_.then() 中注册的回调。这个回调被激活后:
- 将主线程控制权交还给
foo 协程。
- 将
value(即 100)传递给协程。
foo 协程恢复执行,将得到的 100 赋值给变量 a。接着执行后续代码,打印 100 和 2。最后,控制权归还给父协程。
总结与思考
Promise 虽然解决了回调地狱,但其链式调用的 .then() 方法在语义表达上仍有不足。async/await 的出现,使得我们可以用近乎同步的代码风格编写异步逻辑。这背后依赖于生成器(协程)的暂停/恢复能力以及 Promise 的状态管理。
async/await 已成为现代 JavaScript 异步编程的主流风格。实际上,不止 JavaScript,许多其他主流语言如 Python、Dart、C# 等都引入了类似的语法,这不仅让代码更整洁,也确保了函数的异步特性(始终返回 Promise)。
最后,我们通过一道经典的面试题来巩固对事件循环、微任务以及 async/await 执行顺序的理解:
async function foo() {
console.log('foo')
}
async function bar() {
console.log('bar start')
await foo()
console.log('bar end')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
console.log('promise executor')
resolve();
}).then(function () {
console.log('promise then')
})
console.log('script end')
// 输出顺序:
// script start
// bar start
// foo
// promise executor
// script end
// bar end
// promise then
// setTimeout