JavaScript异步编程实例:Promise与async/await代码对比

作者:纸飞机

时间:2026-10-30

浏览:547

深入对比JavaScript中Promise与async/await的语法演进、执行机制、链式调用及错误处理,配合完整实例代码与四幅独立架构图解析。

文章目录

在JavaScript开发中,网络请求、文件读取或延迟计时等操作均属于非阻塞异步任务。处理异步调用的主流方案经历了从回调函数到 Promise,再到 ES2017 引入的 async/await 的演变。后两者并非对立关系,而是底层基础与语法糖的承接关系。本文通过具体的串行请求、错误捕获与并行调度等实例,深入对比两种方式的代码组织结构、控制流差异以及适用场景,帮助读者在实际项目中写出清晰可靠的异步逻辑。

一、异步控制流演进模型

要理解 async/await 为什么更受青睐,首先需要看清两者在语法表达层面对思维模型的重构。Promise 将回调地狱重构成由 .then() 与 .catch() 链接的流式管道,但业务变量依然受限于闭包作用域;而 async/await 则允许开发者以同步顺序编写异步流程,消除了层级嵌套,使得代码结构直接反映业务逻辑的执行次序。

JavaScript异步编程实例:Promise与async/await代码对比
图 1:Promise 链式调用与 async/await 线性控制流的结构对照

二、场景实例一:多步骤串行依赖调用

在典型业务中,我们经常遇到依赖前序结果的链式调用,例如:根据用户 ID 获取用户信息,接着依据用户所在部门 ID 获取部门详情,最后拉取部门公告。如果使用 Promise,需要通过返回值在回调链中不断传递数据,或者借助外部作用域变量暂存数据。

1. Promise 链式写法

在下面的代码中,每个步骤完成后返回一个新的 Promise 对象,下一个 .then() 获取解析后的结果:

// 模拟异步请求函数
function fetchUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: userId, name: "张三", deptId: "D101" }), 100);
  });
}

function fetchDepartment(deptId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: deptId, name: "研发部", manager: "李四" }), 100);
  });
}

function fetchNotice(deptName) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`【${deptName}】本周例会定于周五下午召开`), 100);
  });
}

// Promise 链式执行
function showNoticeWithPromise(userId) {
  let currentUser = null;
  fetchUser(userId)
    .then((user) => {
      currentUser = user;
      return fetchDepartment(user.deptId);
    })
    .then((dept) => {
      return fetchNotice(dept.name);
    })
    .then((notice) => {
      console.log(`通知推送给 ${currentUser.name}: ${notice}`);
    })
    .catch((error) => {
      console.error("加载失败:", error);
    });
}

showNoticeWithPromise("U001");

痛点分析:为了在最后的 .then() 中同时使用第一步得到的 user.name,我们不得不声明一个外部变量 currentUser 来暂存值,破坏了函数的纯净度,也增加了状态维护成本。

2. async/await 改进写法

使用 async/await 后,每一个异步结果都可以直接赋值给局部常量,作用域完全符合普通顺序代码的预期:

async function showNoticeWithAsync(userId) {
  try {
    const user = await fetchUser(userId);
    const dept = await fetchDepartment(user.deptId);
    const notice = await fetchNotice(dept.name);
    
    // 直接访问所有上下文变量,无需外部暂存
    console.log(`通知推送给 ${user.name}: ${notice}`);
  } catch (error) {
    console.error("加载失败:", error);
  }
}

showNoticeWithAsync("U001");

三、执行时序与底层微任务状态变迁

尽管 async/await 书写起来像同步代码,但其底层完全依赖 Promise 与 JavaScript 的事件循环微任务队列(Microtask Queue)。当执行到 await 关键字时,JavaScript 引擎会暂停该 async 函数内部后续语句的执行,将当前上下文挂起,转而执行外层的其他同步脚本;当被等待的 Promise 状态变为已决议(Fulfilled)后,后续代码作为微任务被推入队列等待调度执行。

JavaScript异步编程实例:Promise与async/await代码对比
图 2:Promise 状态机流转与对应回调执行逻辑

四、场景实例二:错误处理机制与颗粒度控制

在异步流程中,错误可能来自网络超时、服务端返回非 200 状态,或者本地数据解析异常。两者的错误处理模式有显著区别:

  • Promise:通过 .catch() 统一捕获链条中任意环节的异常。虽然书写集中,但在链条较长时,难以精准定位到底是哪一步抛出的错误。
  • async/await:直接复用原生的 try...catch 语法,能够自然地包揽同步代码与异步操作中抛出的异常,且支持针对单条语句进行独立包裹,精准隔离故障。

对比代码实例

// 模拟可能出错的请求
function verifyToken(token) {
  return new Promise((resolve, reject) => {
    if (token === "valid-token") {
      resolve({ uid: "1001" });
    } else {
      reject(new Error("Token 已过期或无效"));
    }
  });
}

// 方式一:Promise.catch 集中捕获
function authFlowWithPromise(token) {
  verifyToken(token)
    .then((result) => {
      console.log("验证通过,用户ID:", result.uid);
    })
    .catch((err) => {
      console.error("Promise 捕获异常:", err.message);
    });
}

// 方式二:async/await try-catch 结构
async function authFlowWithAsync(token) {
  try {
    const result = await verifyToken(token);
    console.log("验证通过,用户ID:", result.uid);
  } catch (err) {
    console.error("async/await 捕获异常:", err.message);
  }
}

authFlowWithPromise("invalid");
authFlowWithAsync("invalid");
重要注意事项:在使用 async/await 时,如果省略 try...catch 且没有对返回的 Promise 挂载 .catch(),一旦出现错误,会导致 UnhandledPromiseRejection 警告或直接中断 Node.js 进程。

五、场景实例三:并行执行与顺序排队陷阱

初学者在使用 async/await 时最常见的误区是“滥用串行”。当多个任务彼此独立时,连续写多个 await 会导致原本可以并发进行的请求变成低效的依次排队,成倍增加耗时。

JavaScript异步编程实例:Promise与async/await代码对比
图 3:并发执行(Promise.all)与串行 await 耗时对比图示

并行代码实践与技巧

在实际编码中,针对互不依赖的请求,应当结合 Promise.all 或 Promise.allSettled 与 await 共同使用:

// 模拟三个独立的异步资源
const getNews = () => new Promise(res => setTimeout(() => res(["新闻1", "新闻2"]), 200));
const getWeather = () => new Promise(res => setTimeout(() => res({ temp: 22, text: "晴" }), 150));
const getBanner = () => new Promise(res => setTimeout(() => res("banner_url.jpg"), 100));

// 错误示范:原本无关联的操作被写成了串行排队
async function loadDashboardSlow() {
  const news = await getNews();       // 耗时 200ms
  const weather = await getWeather(); // 再等 150ms
  const banner = await getBanner();   // 再等 100ms
  return { news, weather, banner };   // 累计约 450ms
}

// 推荐做法:通过 Promise.all 并发发起,单次 await 等待全量结果
async function loadDashboardFast() {
  const [news, weather, banner] = await Promise.all([
    getNews(),
    getWeather(),
    getBanner()
  ]);
  return { news, weather, banner };   // 耗时仅约 200ms
}

六、维度对比与技术选型总结

为了在工程开发中做出规范、合理的决策,下表系统梳理了两种语法在核心维度上的特性差异:

对比维度 Promise (.then/.catch) async / await
语法定位 标准内置对象,提供状态机与链式接口 基于 Promise 和生成器的语法糖
代码阅读流 流式链条,依赖回调函数跳转 自上而下的线性顺序,阅读直观
作用域访问 跨步骤使用变量需借助外部高阶作用域暂存 同级函数作用域,普通局部变量自由访问
异常捕获 链尾 .catch() 统一捕获,报错行难以定位 标准 try...catch,支持颗粒度细化
条件与循环 与 if/for 混用较繁琐,常需递归构建链 天然支持 if、for...of、while
并发调度 原生拥有 all, race, allSettled 需配合 Promise.all 等方法使用
JavaScript异步编程实例:Promise与async/await代码对比
图 4:现代 JavaScript 异步方案决策决策树

七、总结与最佳实践

在现代前端与 Node.js 应用程序中,最佳实践并非在两者之间做“二选一”的排他选择,而是分工协作:

  1. 底层封装用 Promise:遇到传统的基于回调(callback)的 API(例如 fs.readFile 或 setTimeout)时,使用 new Promise((resolve, reject) => { ... }) 将其封装为标准 Promise 实例。
  2. 业务主干用 async/await:在组件层、接口调用层和控制器中,优先采用 async/await 配合 try...catch 处理数据流,提高代码的可读性与调试效率。
  3. 并发聚合结合两者:对于多个独立异步任务,使用 await Promise.all([...]),兼具非阻塞并发的高性能与线性书写的整洁感。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章