- Published on
async, await 的执行机制与事件循环
ES2017 引入的 async/await
是 JavaScript 异步编程的革命性特性。它并非一项全新的技术,而是建立在 Promise
和 Generator
之上的语法糖,旨在让我们能够以一种更接近同步代码的、更直观的方式来编写和理解异步逻辑。
async
函数的基本规则
async
函数的返回值
一个 async
函数总是隐式地返回一个 Promise
对象。其最终状态和值由函数体的执行结果决定:
- 正常返回: 如果函数
return
了一个值x
,async
函数会返回一个fulfilled
状态的 Promise,其值为x
。 - 无返回值: 如果函数没有显式
return
,会返回一个fulfilled
状态的 Promise,其值为undefined
。 - 抛出异常: 如果函数内部抛出了一个错误,会返回一个
rejected
状态的 Promise,其 reason 就是抛出的那个错误。 - 返回 Promise: 如果函数
return
了一个 Promise,那么async
函数的返回值就是那个 Promise。
await
的使用环境
await
关键字只能在 async
函数内部使用。
一个例外:顶层
await
在现代 JavaScript 模块 (type="module"
) 的顶层作用域中,可以使用顶层 await
(Top-Level Await),而无需将其包裹在 async
函数中。
// 假设有一个返回 Promise 的函数,模拟获取数据
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve("从服务器获取的数据"), 1000);
});
}
// 顶层 await 直接在模块顶层使用
const data = await fetchData();
console.log(data); // 1秒后输出: 从服务器获取的数据
await
的核心执行机制:暂停与恢复
await
类似于 Generator
中的 yield
,是一个暂停点,但其行为与 Promise 和微任务队列紧密相连。
暂停与控制权交还
当 async
函数的执行流遇到 await expression
时:
- 执行表达式: 引擎会首先同步地执行
await
右侧的expression
。 - 暂停函数: 在
expression
执行完毕并返回一个值(通常是一个 Promise)后,async
函数的执行才会暂停。 - 交还控制权: 控制权被立即交还给调用
async
函数的上层执行上下文,允许主线程继续执行后续的同步代码,因此它不会阻塞主线程。
恢复与微任务队列
当 await
一个 Promise
时,JavaScript 引擎在后台实际上为这个 Promise
注册了一个回调 (通过 .then()
)。async
函数中 await
语句之后的所有代码,都被置于这个回调中。因此,async
函数的“恢复”执行部分,会被放入微任务队列,等待当前宏任务及其所有同步代码执行完毕后,由事件循环负责处理。
代码示例与执行分析
// 一个返回 Promise 的函数,模拟异步操作
function delay(ms) {
return new Promise(resolve => {
// setTimeout 会将 resolve 的调用安排到未来的一个宏任务中
setTimeout(() => resolve(`Resolved after ${ms}ms`), ms);
});
}
async function asyncFunction() {
// 对应图中的步骤 2
console.log('B: async 函数开始');
// 对应图中的步骤 3 & 4
const result = await delay(0);
// await 让函数在此暂停,并将后续代码放入微任务
// 对应图中的步骤 11
console.log(`D: ${result}`);
console.log('E: async 函数结束');
}
console.log('A: 主线程脚本开始');
// 对应图中的步骤 1
asyncFunction();
// 对应图中的步骤 5 & 6
console.log('C: 主线程脚本结束');
代码的实际输出顺序:
A: 主线程脚本开始
B: async 函数开始
C: 主线程脚本结束
D: Resolved after 0ms
E: async 函数结束
执行顺序分解:
A: 主线程脚本开始
被打印。asyncFunction()
被调用 (图步骤1),进入函数体,打印B: async 函数开始
(图步骤2)。- 遇到
await delay(0)
(图步骤3)。delay(0)
被执行,一个setTimeout
被安排到宏任务队列。asyncFunction
在此暂停,并将控制权交还给主线程 (图步骤4 & 5)。 - 主线程继续执行,打印
C: 主线程脚本结束
(图步骤6)。至此,第一个宏任务(即主脚本)执行完毕。 - 事件循环检查微任务队列(当前为空),然后去宏任务队列取出
setTimeout
的回调并执行。 setTimeout
的回调执行resolve()
,delay
函数返回的 Promise 状态变为fulfilled
(图步骤7)。- Promise 的
fulfilled
状态,将asyncFunction
中await
后续的代码作为一个微任务推入微任务队列 (图步骤8)。 setTimeout
宏任务执行完毕。事件循环检查微任务队列,发现有一个任务。- 事件循环取出该微任务并执行,恢复
asyncFunction
的执行 (图步骤9 & 10)。 await
表达式“解包”Promise 的结果,result
被赋值。- 打印
D: Resolved after 0ms
和E: async 函数结束
(图步骤11)。
await
的返回值与错误处理
- “解包” Promise:
await
表达式会“解开”或“提取” Promise 的结果。- 如果 Promise 变为
fulfilled
,await
表达式的值就是那个fulfilled
的值。 - 如果
await
右侧不是一个 Promise,它会像Promise.resolve()
一样,将其值直接返回。
- 如果 Promise 变为
- 抛出错误: 如果 Promise 变为
rejected
,await
表达式会抛出那个rejected
的原因 (reason)。
try...catch
这个“抛出错误”的特性,使得我们可以用非常自然的、同步风格的 try...catch
块来捕获和处理异步操作中发生的错误,这是对 .then().catch()
链式写法的重大改进。
async function fetchData() {
try {
// await 会暂停函数,等待 Promise 完成
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
// await 会“解包” response.json() 返回的 Promise
const data = await response.json();
return data;
} catch (error) {
// 如果 fetch 或 .json() 任何一个环节的 Promise 被 reject,
// await 会将其作为异常抛出,被这里的 catch 捕获。
console.error('Fetch error:', error);
}
}