JavaScript 异步编程之 async/await 深入剖析

608 阅读8分钟

你好,我是木亦。在 JavaScript 中,异步编程已然成为每位开发者都必须攻克的关键领域。随着 JavaScript 应用的边界不断拓展,无论是构建交互性强的网页应用,还是开发复杂的后端服务,在处理网络请求、文件读取这类耗时操作时,异步编程的重要性愈发举足轻重。

而 async/await 作为 ES2017 引入的强大异步编程语法糖,为我们编写和理解异步代码开辟了一条更加简洁、直观的道路。

这篇文章会跟你一起探索 async/await 的底层原理与精妙之处。

一、异步编程的前世今生

JavaScript 以单线程模式运行,这意味着它在同一时间只能执行一项任务。倘若所有任务都按部就班地同步执行,一旦遭遇网络请求、文件读取等耗时较长的操作,整个程序就会陷入阻塞状态,网页界面也会随之卡顿,这无疑会对用户体验造成极大的负面影响。为了打破这一困境,异步编程应运而生。它允许在执行耗时操作时,主线程不会被阻塞,而是能够继续处理其他任务。待耗时操作完成后,再通过回调函数、Promise 或者 async/await 等方式来处理操作结果,从而实现程序的高效运行与流畅交互。

二、Promise:异步编程的基石

在深入探究 async/await 之前,我们有必要先回顾一下 Promise。Promise 是 ES6 引入的一种用于处理异步操作的核心机制,它代表着一个异步操作的最终完成状态(成功或失败)以及对应的结果值。Promise 存在三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦 Promise 的状态发生改变,就无法再回溯,状态转变仅存在两种可能:从 pending 转变为fulfilled,或者从 pending 转变为 rejected

下面来看一个简单的 Promise 示例:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功');
    } else {
      reject('操作失败');
    }
  }, 1000);
});
promise.then((value) => {
  console.log(value); // 操作成功
}).catch((error) => {
  console.log(error);
});

在这个示例中,我们创建了一个 Promise,它会在 1 秒后根据 success 的值来决定是调用 resolve 函数表示操作成功,还是调用 reject 函数表示操作失败。随后,通过then方法来处理操作成功的情况,通过 catch 方法来捕获并处理操作失败时的错误。

Promise 的出现成功解决了回调地狱的难题,使得异步代码的结构更加清晰、易于维护。然而,在处理多个异步操作时,链式调用的代码结构有时仍显得较为繁琐,而 async/await 正是在 Promise 的基础上进一步优化而来,旨在提供更加简洁、高效的异步编程体验。

三、async/await 基础语法详解

async关键字用于定义一个异步函数,这类函数始终会返回一个 Promise 对象。而await关键字则只能在async函数内部使用,它的作用是等待一个 Promise 对象的状态变为 fulfilledrejected,并暂停async函数的执行,直到所等待的 Promise 被解决。

以下是一个简单的 async/await 示例:

async function asyncFunction() {
  try {
    const value = await new Promise((resolve) => {
      setTimeout(() => {
        resolve('异步操作结果');
      }, 1000);
    });
    console.log(value); // 异步操作结果
  } catch (error) {
    console.log(error);
  }
}

asyncFunction();

在这个示例中,asyncFunction 是一个异步函数,其中的 await 关键字等待 Promise 的解决,待 Promise 成功解决后,将结果赋值给value变量,最后将结果打印输出。如果 Promise 被拒绝,catch 代码块会捕获并处理相应的错误信息。

四、async/await 的显著优势

  1. 代码简洁直观:async/await 使得异步代码的书写风格与同步代码极为相似,大大提升了代码的可读性与可维护性。开发者无需再花费大量精力去梳理复杂的异步操作流程,代码逻辑更加清晰明了。
  2. 错误处理便捷高效:借助 try...catch 代码块,我们可以统一处理异步操作过程中可能出现的各种错误,而无需像使用 Promise 时那样通过链式调用catch方法来捕获错误,这使得错误处理更加集中、高效。
  3. 顺序执行一目了然:await 关键字会暂停 async 函数的执行,直至所等待的 Promise 被成功解决,这使得异步操作的顺序执行更加直观、易于理解,有效避免了因异步操作顺序混乱而导致的逻辑错误。

五、async/await 与 Promise 的协同应用

async/await 是建立在 Promise 基础之上的,因此它们能够完美地协同工作。在实际应用中,我们既可以在async函数内部返回一个 Promise 对象,也可以在使用await等待一个 Promise 对象解决后,继续进行链式操作。

以下是一个展示 async/await 与 Promise 结合使用的示例:

async function asyncPromiseCombination() {
  const result1 = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('第一个异步操作结果');
    }, 1000);
  });
  console.log(result1);
  const result2 = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('第二个异步操作结果');
    }, 1000);
  });
  console.log(result2);
  return result1 +'' + result2;
}

asyncPromiseCombination().then((finalResult) => {
  console.log(finalResult); // 第一个异步操作结果 第二个异步操作结果
});

在这个示例中,我们在 async 函数 asyncPromiseCombination 中依次执行了两个异步操作,通过 await 关键字分别等待它们的结果。待两个异步操作都完成后,将两个结果拼接起来并返回。最后,在外部通过 then 方法来处理最终拼接后的结果。

六、async/await 进阶技巧

(一)并发与并行操作

在处理多个异步任务时,我们可以利用 Promise.all 来实现并发操作。Promise.all 接受一个 Promise 数组作为参数,当所有 Promise 都被解决时,它返回一个包含所有结果的新 Promise;只要有一个 Promise 被拒绝,整个 Promise.all 就会被拒绝。

async function concurrentTasks() {
  const promise1 = new Promise((resolve) => setTimeout(() => resolve('任务1结果'), 1000));
  const promise2 = new Promise((resolve) => setTimeout(() => resolve('任务2结果'), 2000));
  const [result1, result2] = await Promise.all([promise1, promise2]);
  console.log(result1, result2);
}

如果希望所有任务都能执行完成,无论成功或失败,可以使用 Promise.allSettled。它会返回一个新的 Promise,该 Promise 在所有输入的 Promise 都已被解决(包括成功和失败)时被解决,返回的结果数组中每个元素都会包含任务的状态(status 为 fulfilledrejected)以及对应的结果或错误。

async function allSettledTasks() {
  const promise1 = new Promise((resolve) => setTimeout(() => resolve('任务1结果'), 1000));
  const promise2 = new Promise((reject) => setTimeout(() => reject('任务2失败'), 2000));
  const results = await Promise.allSettled([promise1, promise2]);
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`任务${index + 1}成功,结果:`, result.value);
    } else {
      console.log(`任务${index + 1}失败,错误:`, result.reason);
    }
  });
}

(二)处理异步迭代器

async/await 可以很好地与异步迭代器配合使用。异步迭代器是一种特殊的迭代器,它的 next() 方法返回一个 Promise。例如,我们有一个异步生成器函数,它会异步生成一系列数字:

async function* asyncGenerator() {
  yield await new Promise((resolve) => setTimeout(() => resolve(1), 1000));
  yield await new Promise((resolve) => setTimeout(() => resolve(2), 1000));
  yield await new Promise((resolve) => setTimeout(() => resolve(3), 1000));
}
async function iterateAsyncGenerator() {
  const gen = asyncGenerator();
  let result = await gen.next();
  while (!result.done) {
    console.log(result.value);
    result = await gen.next();
  }
}

七、async/await 调试技巧

(一)使用 debugger 关键字

在 async 函数中,我们可以使用 debugger 关键字来暂停代码执行,以便在调试工具中查看变量状态和执行流程。例如:

async function debugAsyncFunction() {
  const value = await new Promise((resolve) => setTimeout(() => resolve('异步操作结果'), 1000));
  debugger;
  console.log(value);
}

当代码执行到 debugger 时,会在浏览器的开发者工具或 Node.js 的调试环境中暂停,我们可以查看value的值以及函数调用栈等信息。

(二)合理使用日志输出

在 async 函数中,通过 console.log、console.error 等日志输出函数,可以帮助我们了解异步操作的执行过程和结果。比如在错误处理时,详细的错误日志可以帮助我们快速定位问题:

async function logAsyncFunction() {
  try {
    const value = await new Promise((resolve, reject) => {
      setTimeout(() => reject('异步操作失败'), 1000);
    });
    console.log(value);
  } catch (error) {
    console.error('异步操作出现错误:', error);
  }
}

(三)利用调试工具的断点功能

无论是浏览器的开发者工具还是 Node.js 的调试工具,都支持设置断点。我们可以在 async 函数的关键位置设置断点,逐步调试代码,观察变量的变化和异步操作的执行情况。例如在 Chrome 浏览器中,在 async 函数的代码行左侧点击即可设置断点,然后刷新页面或执行函数,代码就会在断点处暂停。

async/await 无疑为 JavaScript 异步编程带来了前所未有的便利,它让我们能够以更加优雅、简洁的方式处理复杂的异步操作。通过深入理解其原理与用法,掌握进阶技巧和调试方法,我们能够编写出更加高效、易读的异步代码,为打造高性能的 JavaScript 应用奠定坚实基础。希望本文能帮助大家更好地掌握 async/await 这一强大工具,在 JavaScript 编程的道路上更进一步。