异步 JavaScript:从回调地狱到异步和等待

已发表: 2022-03-11

编写成功的 Web 应用程序的关键之一是能够在每个页面上进行数十次 AJAX 调用。

这是一个典型的异步编程挑战,您如何选择处理异步调用将在很大程度上决定您的应用程序的成败,进而可能影响您的整个启动。

长期以来,在 JavaScript 中同步异步任务是一个严重的问题。

这一挑战对使用 Node.js 的后端开发人员的影响与对使用任何 JavaScript 框架的前端开发人员的影响一样大。 异步编程是我们日常工作的一部分,但挑战往往被轻视,没有在正确的时间考虑。

异步 JavaScript 简史

第一个也是最直接的解决方案是以作为回调的嵌套函数的形式出现的。 这个解决方案导致了一种叫做回调地狱的东西,太多的应用程序仍然觉得它很痛苦。

然后,我们得到了Promises 。 这种模式使代码更易于阅读,但与不要重复自己 (DRY) 原则相去甚远。 在很多情况下,您必须重复相同的代码才能正确管理应用程序的流程。 最新添加的 async/await 语句形式终于使 JavaScript 中的异步代码与任何其他代码一样易于读写。

让我们看一下每个解决方案的示例,并反思 JavaScript 中异步编程的演变。

为此,我们将检查一个执行以下步骤的简单任务:

  1. 验证用户的用户名和密码。
  2. 获取用户的应用程序角色。
  3. 记录用户的应用程序访问时间。

方法一:回调地狱(“末日金字塔”)

同步这些调用的古老解决方案是通过嵌套回调。 对于简单的异步 JavaScript 任务来说,这是一种不错的方法,但由于称为回调地狱的问题而无法扩展。

插图:异步 JavaScript 回调地狱反模式

这三个简单任务的代码如下所示:

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

每个函数都有一个参数,该参数是另一个函数,该函数使用参数调用,该参数是前一个操作的响应。

太多的人仅仅阅读上面的句子就会经历大脑冻结。 拥有一个包含数百个类似代码块的应用程序会给维护代码的人带来更多麻烦,即使他们是自己编写的。

一旦您意识到database.getRoles是另一个具有嵌套回调的函数,这个示例就会变得更加复杂。

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

除了代码难以维护之外,DRY 原则在这种情况下绝对没有任何价值。 例如,在每个函数中重复错误处理,并且从每个嵌套函数调用主回调。

更复杂的异步 JavaScript 操作,例如循环异步调用,是一个更大的挑战。 事实上,使用回调没有简单的方法可以做到这一点。 这就是为什么像 Bluebird 和 Q 这样的 JavaScript Promise 库如此受欢迎的原因。 它们提供了一种对语言本身尚未提供的异步请求执行常见操作的方法。

这就是原生 JavaScript Promises 的用武之地。

JavaScript 承诺

Promise 是逃离回调地狱的下一个合乎逻辑的步骤。 这种方法并没有取消回调的使用,但它使函数链接变得简单并简化了代码,使其更易于阅读。

插图:异步 JavaScript Promise 图

有了 Promises,我们的异步 JavaScript 示例中的代码将如下所示:

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

为了实现这种简单性,示例中使用的所有函数都必须是Promisified 。 让我们看一下如何更新getRoles方法以返回Promise

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

我们修改了该方法以返回一个带有两个回调的Promise ,并且Promise本身执行该方法中的操作。 现在, resolvereject回调将分别映射到Promise.thenPromise.catch方法。

您可能会注意到getRoles方法在内部仍然容易出现末日金字塔现象。 这是由于创建数据库方法的方式,因为它们不返回Promise 。 如果我们的数据库访问方法也返回Promise ,那么getRoles方法将如下所示:

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

方法 3:异步/等待

随着 Promises 的引入,厄运金字塔得到了显着缓解。 但是,我们仍然必须依赖传递给Promise.then.catch方法的回调。

Promise 为 JavaScript 中最酷的改进之一铺平了道路。 ECMAScript 2017 以asyncawait语句的形式在 JavaScript 的 Promise 之上引入了语法糖。

它们允许我们编写基于Promise的代码,就好像它是同步的一样,但不会阻塞主线程,如下代码示例所示:

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

Awaiting Promise to resolve 只允许在async函数中使用,这意味着verifyUser必须使用async function来定义。

但是,一旦进行了这个小更改,您就可以await任何Promise ,而无需对其他方法进行额外更改。

异步 - 一个期待已久的承诺解决方案

异步函数是 JavaScript 异步编程发展的下一个合乎逻辑的步骤。 它们将使您的代码更清晰,更易于维护。 将函数声明为async将确保它始终返回Promise ,因此您不必再担心这一点。

为什么要从今天开始使用 JavaScript async函数?

  1. 生成的代码更干净。
  2. 错误处理要简单得多,它依赖于try / catch ,就像在任何其他同步代码中一样。
  3. 调试要简单得多。 在.then块内设置断点不会移动到下一个.then因为它只会逐步执行同步代码。 但是,您可以单步await调用,就好像它们是同步调用一样。