async/await语法糖简洁明了,但在一种情况下不推荐使用

409 阅读4分钟

厌倦了回调,想拥抱 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 这种语法糖在生产上的代码可读性非常的差劲 👎🏻

示例

原始代码:简洁,对开发人员非常友好

img_2025_05_27_07_59_04.png

但是打包部署到生产环境之后,变得难以阅读:

img_2025_05_27_07_59_47.png

场景:你遇到了一个客户环境才能复现的 bug,客户可能是内网环境只能通过 vpn 或堡垒机访问。在不能启动 source-map 的情况去调试排查问题,那么就要在浏览器的开发者面板中源码打断点调试。若遇到了 async/await 的代码,稀碎的结构不好阅读,对调试非常不友好

但是 promise.then 的代码,打包后基本不变(可能会有 polyfill)。Promise 只需要确保目标环境中有 Promise 对象(通过 polyfill)即可,语法结构不需要大改

总结一下

  • Promise 是对象,只需提供 polyfill 即可
  • async/await 是语法糖,需要转换为复杂的状态机实现

对于 async/await 你的实践经验是怎么样的,欢迎在评论区留言,一起讨论 💬