厌倦了回调,想拥抱 async/await 的简洁?
本文不仅带你理解 async/await 的核心原理,如 Promise 语法糖和状态机,还会展示其在实际开发中的应用
更重要的是,基于我的实践经验,我将为你揭示何时应谨慎使用 async/await,以确保代码在生产环境中的清晰度
async/await底层原理
1. 基于 Promise 的语法糖
Promise 是对象:Promise 是 ES6 引入的一个内置对象,类似于 Array、Object 等
async/await 是语法糖:是在 ES2017 (ES8) 引入的语法糖,本质上是 Generator 和 Promise 的组合使用
// async函数实际上返回一个Promise
async function example() {
return "Hello";
}
// 等价于
function example() {
return Promise.resolve("Hello");
}
2. 状态机转换
JavaScript 引擎将 async/await 转换为状态机,每个 await 点都是一个状态转换:
async function fetchData() {
console.log("1. 开始");
const data = await fetch("/api/data"); // 暂停点1
console.log("2. 获取数据");
const result = await data.json(); // 暂停点2
console.log("3. 解析完成");
return result;
}
底层转换类似于:
function fetchData() {
return new Promise((resolve, reject) => {
function step(state, value) {
switch (state) {
case 0:
console.log("1. 开始");
return fetch("/api/data").then(data => step(1, data));
case 1:
console.log("2. 获取数据");
return value.json().then(result => step(2, result));
case 2:
console.log("3. 解析完成");
resolve(value);
break;
}
}
step(0);
});
}
通过递归调用 step 函数来实现链式的效果,通过 switch case 来实现状态的切换,结合 promise 的基础功能做到异步的串行执行
原理看着很清晰明了,但是看看 babel 降级过后的真正执行的代码 😂:
降级之后,增加了加入辅助函数和状态机逻辑,代码体积显著增加
// 省略了部分代码外部代码内容
// function _asyncToGenerator(fn)
// function asyncGeneratorStep
// function _regeneratorRuntime()
// function _typeof(obj)
function fetchData() {
return _fetchData.apply(this, arguments);
}
function _fetchData() {
_fetchData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
var data, result;
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
console.log("1. 开始");
_context.next = 3;
return fetch("/api/data");
case 3:
data = _context.sent;
// 暂停点1
console.log("2. 获取数据");
_context.next = 7;
return data.json();
case 7:
result = _context.sent;
// 暂停点2
console.log("3. 解析完成");
return _context.abrupt("return", result);
case 10:
case "end":
return _context.stop();
}
}, _callee);
}));
return _fetchData.apply(this, arguments);
}
3. 事件循环机制
await 会将函数执行权交还给事件循环,等待 Promise 完成:
async function demo() {
console.log("1");
await Promise.resolve();
console.log("3");
}
console.log("0");
demo();
console.log("2");
// 输出顺序:0, 1, 2, 3
具体应用场景
1. API 请求处理
// 传统Promise方式
function getUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.catch(error => console.error(error));
}
// async/await方式
async function getUserData(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error("获取用户数据失败:", error);
throw error;
}
}
将原本的 promise.then 的链式调用,转换为 async/await 的同步写法,代码可读性更好,逻辑更清晰
2. 文件操作(Node.js)
const fs = require('fs').promises;
async function processFiles() {
try {
// 并行读取多个文件
const [file1, file2, file3] = await Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
]);
// 处理文件内容
const combinedContent = file1 + file2 + file3;
// 写入结果
await fs.writeFile('combined.txt', combinedContent);
console.log("文件处理完成");
} catch (error) {
console.error("文件操作失败:", error);
}
}
最佳实践
就我的经验而言,能用 promise 可读性足够好的情况下,都直接使用 promise,不要使用 async/await 比如下面的红绿灯的例子就非常适合使用 async/await
/**
* 1. 循环打印红黄绿
* 红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
*/
console.log("1. 循环打印红黄绿");
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function blue() {
console.log("blue");
}
function task(fn, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
fn();
resolve();
}, delay);
});
}
如果是 promise 的写法,代码如下:
function rgb() {
return task(red, 3000)
.then(() => task(green, 2000))
.then(() => task(blue, 1000))
.then(() => rgb());
}
逻辑看着非常的绕头,但是使用 async/await 的写法,代码如下:
async function rgb3() {
await task(red, 3000);
await task(green, 2000);
await task(blue, 1000);
rgb3();
}
简单清晰,递归调用也非常好理解。像是这样的场景就非常的适合使用 async/await
优点在于开发的时候,如果出生产就不那么回事了
因为兼容性问题,在 es6 的降级为 es5 的代码后,async/await 这种语法糖在生产上的代码可读性非常的差劲 👎🏻
示例
原始代码:简洁,对开发人员非常友好
但是打包部署到生产环境之后,变得难以阅读:
场景:你遇到了一个客户环境才能复现的 bug,客户可能是内网环境只能通过 vpn 或堡垒机访问。在不能启动 source-map 的情况去调试排查问题,那么就要在浏览器的开发者面板中源码打断点调试。若遇到了 async/await 的代码,稀碎的结构不好阅读,对调试非常不友好
但是 promise.then 的代码,打包后基本不变(可能会有 polyfill)。Promise 只需要确保目标环境中有 Promise 对象(通过 polyfill)即可,语法结构不需要大改
总结一下
- Promise 是对象,只需提供 polyfill 即可
- async/await 是语法糖,需要转换为复杂的状态机实现
对于 async/await 你的实践经验是怎么样的,欢迎在评论区留言,一起讨论 💬