返回

用 async/await 掌控异步世界的缰绳

前端

前言

时光荏苒,转眼间自上篇博文问世已过两个月有余。这两个月里,本人一直忙于实习面试之事,以至于文章产出方面有所耽搁。如今,总算功夫不负有心人,成功斩获某大厂实习 offer 一枚,堪称 2021 年开年头彩(窃喜)。闲话少叙,且接上篇《实现一个符合 Promise/A+规范的 Promise 库》继续前行。

在上一篇文章中,我们对 Promise 的基本原理和实现细节进行了详细的剖析。对于初学者来说,掌握了 Promise 的知识无疑如获至宝。然而,在实际的编程实践中,我们常常需要处理更加复杂的异步任务,而传统的回调函数或 Promise 的嵌套使用会使代码变得难以维护和理解。为了解决这个问题,JavaScript 引入了 async/await ,它可以让我们使用同步的语法来编写异步代码,从而极大地简化了异步编程的复杂性。

缘起:异步编程的痛点

在理解 async/await 的妙用之前,让我们先回顾一下传统的异步编程方式——回调函数和 Promise。回调函数是一种将函数作为参数传递给另一个函数,以便在某个事件发生后执行该回调函数的编程技术。这种方式看似简单,但当嵌套层级过深时,就会导致难以理解和维护的“回调地狱”。

const readFile = (path, callback) => {
  // 异步读取文件
  fs.readFile(path, (err, data) => {
    if (err) {
      callback(err);
      return;
    }

    // 继续处理数据
    const result = parseData(data);
    callback(null, result);
  });
};

readFile('data.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }

  // 继续处理结果
  const finalResult = processResult(result);
  console.log(finalResult);
});

为了解决回调地狱的问题,Promise 应运而生。Promise 是一个对象,它代表着异步操作的最终完成或失败的状态。我们可以使用 .then().catch() 方法来处理 Promise 的结果,从而使异步编程更加清晰和可控。

const readFile = (path) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) {
        reject(err);
        return;
      }

      resolve(data);
    });
  });
};

readFile('data.txt')
  .then(data => {
    const result = parseData(data);
    return result;
  })
  .then(result => {
    const finalResult = processResult(result);
    console.log(finalResult);
  })
  .catch(err => {
    console.error(err);
  });

虽然 Promise 在一定程度上缓解了回调地狱的问题,但它仍然存在着嵌套层级过深的问题,尤其是在需要处理多个异步任务时。例如,以下代码演示了如何使用 Promise 并发地执行多个异步任务:

const tasks = [
  readFile('data1.txt'),
  readFile('data2.txt'),
  readFile('data3.txt'),
];

Promise.all(tasks)
  .then(results => {
    const finalResults = [];
    results.forEach(result => {
      finalResults.push(processResult(result));
    });
    console.log(finalResults);
  })
  .catch(err => {
    console.error(err);
  });

虽然这段代码看起来比使用回调函数更加清晰和可控,但它仍然存在着嵌套层级过深的问题,而且处理多个异步任务时,代码的可读性和可维护性也会受到影响。

async/await 的登场

为了解决 Promise 嵌套层级过深的问题,JavaScript 引入了 async/await 关键字。async/await 允许我们使用同步的语法来编写异步代码,从而使代码更加清晰和可读。

语法简介

要使用 async/await,首先需要使用 async 关键字修饰一个函数,表示该函数是一个异步函数。在异步函数中,我们可以使用 await 关键字来暂停函数的执行,直到异步操作完成。

async function readFile(path) {
  const data = await fs.readFile(path);
  return data;
}

在上面的例子中,readFile 函数是一个异步函数,它使用 await 关键字来暂停函数的执行,直到文件读取操作完成。一旦文件读取操作完成,函数将继续执行并返回读取到的数据。

使用实例

以下是一个使用 async/await 来实现并行任务的例子:

async function main() {
  const tasks = [
    readFile('data1.txt'),
    readFile('data2.txt'),
    readFile('data3.txt'),
  ];

  const results = await Promise.all(tasks);

  const finalResults = [];
  results.forEach(result => {
    finalResults.push(processResult(result));
  });

  console.log(finalResults);
}

main();

在上面的例子中,main 函数是一个异步函数,它使用 await 关键字来暂停函数的执行,直到所有异步任务都完成。一旦所有异步任务都完成,函数将继续执行并打印最终结果。

优势与局限

async/await 具有以下优点:

  • 代码更具可读性和可维护性 :async/await 允许我们使用同步的语法来编写异步代码,从而使代码更加清晰和易于理解。
  • 减少嵌套层级 :async/await 可以帮助我们减少异步代码的嵌套层级,从而使代码更加易于阅读和维护。
  • 提高代码的可测试性 :async/await 使得异步代码更加容易测试,因为我们可以使用熟悉的同步测试技术来测试异步代码。

然而,async/await 也存在以下局限:

  • 仅限于异步函数 :async/await 只能用于异步函数中,这意味着我们不能在普通函数中使用它们。
  • 需要编译器支持 :async/await 需要编译器支持,这意味着我们不能在所有环境中使用它们。

结语

async/await 是 JavaScript 中一项非常强大的特性,它可以帮助我们编写更加清晰、可读和可维护的异步代码。通过熟练掌握 async/await,我们可以轻松驾驭异步编程的复杂性,并编写出更加优雅和高效的代码。

最后,希望这篇文章能对大家有所帮助。如果您有任何问题或建议,欢迎在评论区留言。感谢您的阅读!