异步 JavaScript 初学者指南

JavaScript、承诺、AsyncAwait
异步 JavaScript 初学者指南 cover image

如果您刚刚开始编程,很可能您正在将程序视为一组连续的逻辑块,其中每个块执行特定的操作并传递其结果,以便下一个块可以运行,依此类推。大部分你是对的,大多数程序都是按顺序运行的,这种模型允许我们构建易于编写和维护的程序。然而,在某些特定的用例中,这种顺序模型不起作用,或者不是最佳的。作为一个例子,考虑一个图书阅读器应用程序。该应用程序具有一些高级功能,例如查找单词的所有出现位置、在书签之间导航等。现在假设用户正在阅读一本很长的书,并决定查找所有出现的常见单词(例如“The”)。应用程序通常需要几秒钟的时间来查找并索引所有出现的该单词。在顺序程序中,在完成搜索操作之前,用户无法与应用程序交互(更改页面或突出显示文本)。希望您能看到这不是最佳的用户体验!

1

该图说明了图书阅读器应用程序的典型执行流程。如果用户启动一个长时间运行的操作(在本例中是在一本大书中搜索所有出现的“the”),则应用程序会在该操作的整个持续时间内“冻结”。在这种情况下,用户将一直点击下一个书签按钮而没有任何结果,直到搜索操作完成,所有操作将立即生效,给最终用户带来应用程序滞后的感觉。

您可能已经注意到,这个示例并不真正对应于我们之前介绍的顺序模型。这是因为这里的操作是相互独立的。用户不需要知道“the”出现的次数来导航到下一个书签,因此操作的执行顺序并不重要。我们不必等待搜索操作结束就可以导航到下一个书签。对之前执行流程的一个可能的改进是基于这个逻辑:我们可以在后台运行长搜索操作,继续处理任何传入的操作,一旦长操作完成,我们可以简单地通知用户。执行流程变为如下:

2

通过这个执行流程,用户体验得到了显着的提升。现在,用户可以启动长时间运行的操作,继续正常使用应用程序,并在操作完成后收到通知。这是异步编程的基础。

Javascript 和其他语言一样,通过提供广泛的 API 来支持这种异步编程风格,以实现您能想到的几乎任何异步行为。归根结底,Javascript 本质上应该是一种异步语言。如果我们参考前面的示例,异步逻辑是所有用户交互应用程序的基础,而 Javascript 主要是为了在浏览器上使用而构建的,其中大多数程序都是关于响应用户操作的。

下面将为您简要介绍异步Javascript:

回调

在典型的程序中,您通常会发现许多函数。要使用函数,我们使用一组参数来调用它。函数代码将执行并返回结果,没有什么异常。异步编程稍微改变了这种逻辑。回到图书阅读器应用程序的示例,我们不能使用常规函数来实现搜索操作逻辑,因为该操作需要未知的时间。常规函数基本上会在操作完成之前返回,这不是我们期望的行为。解决方案是指定另一个函数,搜索操作完成后将执行该函数。这对我们的用例进行了建模,因为我们的程序可以正常继续其流程,并且一旦搜索操作完成,将执行指定的函数以通知用户搜索结果。这个函数就是我们所说的回调函数:

// Search occurrences function
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in result variable
    //....
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}

// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
  if (err) {
    console.log(`Search operation for ${word} ended with an error`);
  } else console.log(`Search results for ${word}: ${result}`);
  return;
}

searchOccurrences("the", handleSearchOccurrencesResult);

首先,我们定义搜索操作函数searchOccurrences。它需要要搜索的单词和第二个参数“callback”,该参数将是搜索操作完成后要执行的函数。搜索操作函数故意保持抽象,我们只需要关注它的两种可能的结果:第一种情况是一切顺利,我们在 result 变量中得到搜索结果。在这种情况下,我们只需使用以下参数调用回调函数:第一个参数为 null 意味着没有发生错误,第二个参数是搜索到的单词,第三个参数可能是这三个参数中最重要的参数,是搜索操作的结果。

第二种情况是发生错误,这也是搜索操作执行完毕,我们必须调用回调函数的情况。我们使用 try 和 catch 块来拦截任何错误,并且只需使用 catch 块中的错误对象调用回调函数。

然后我们定义了回调函数handleSearchOccurrences,我们使其逻辑非常简单。只需将消息打印到控制台即可。我们首先检查“err”参数,看看main函数中是否发生了错误。在这种情况下,我们只是让用户知道搜索操作以错误结束。如果没有出现错误,我们将打印一条包含搜索操作结果的消息。

最后,我们用单词“the”调用 searchOccurrences 函数。该函数现在将正常运行,不会阻塞主程序,一旦搜索完成,回调将被执行,我们将收到结果消息,包括搜索结果或错误消息。

这里值得一提的是,我们只能访问 main 函数和回调函数中的 result 变量。如果我们尝试这样的事情:

let result;
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in searchResult variable
    //....
    result = searchResult;
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);

打印的结果将是未定义的,因为程序不等待 searchOccurrences 函数执行。在主函数内分配结果变量之前,它会移至下一条指令,即打印语句。因此,我们将打印未分配的结果变量。

所以基于这个逻辑,我们应该将所有使用result变量的代码保留在回调函数中。现在这似乎不是问题,但它可能很快就会升级为真正的问题。想象一下,我们有一系列需要按顺序运行的异步函数,在典型的回调逻辑中,您将实现如下所示的内容:

functionA(function (err, resA) {
  ///......
  functionB(resA, function (err, resB) {
    ///......
    functionC(resB, function (err, resC) {
      ///......
      functionD(resC, function (err, resD) {
        ///......
      });
    });
  });
});

请记住,每个回调都有一个错误参数,并且每个错误都必须单独处理。这使得上面已经很复杂的代码变得更加复杂并且难以维护。希望 Promise 能够解决回调地狱问题,我们接下来会介绍这个问题。

承诺

Promise 建立在回调之上,并以类似的方式运行。它们作为 ES6 功能的一部分引入,是为了解决一些回调的明显问题,例如回调地狱。 Promise 提供了自己的函数,这些函数在成功完成(解决)和发生错误(拒绝)时运行。下面展示了使用 Promise 实现的 searchOccurrences 示例:

// Search occurrences function
function searchOccurrences(word) {
  return new Promise((resolve, reject) => {
    try {
      // search operation logic, result is in result variable
      //....
      resolve(word, result);
    } catch (err) {
      reject(err);
    }
  });
}

searchOccurrences("the")
  .then((word, result) => {
    console.log(`Search results for ${word}: ${result}`);
  })
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

让我们回顾一下我们应用的更改:

searchOccurrences 函数返回一个承诺。在 Promise 中,我们保持相同的逻辑:我们有两个函数resolve和reject来代表我们的回调,而不是有一个回调函数来处理成功执行和错误执行。 Promise 将两个结果分开,并在调用 main 函数时提供干净的语法。解析函数使用“then”关键字“挂钩”到主函数。这里我们只指定resolve函数的两个参数并打印搜索结果。类似的事情也适用于拒绝函数,可以使用“catch”关键字来挂钩它。希望您能够体会到 Promise 在代码可读性和整洁性方面提供的优势。如果您仍在争论,请查看我们如何通过将异步函数链接在一起以逐个运行来解决回调地狱问题:

searchOccurrences("the")
  .then(searchOccurrences("asynchronous"))
  .then(searchOccurrences("javascript"))
  .then(searchOccurrences("guide"))
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

异步/等待

Async/Await 是我们 Javascript 异步工具带的最新成员。它们是随 ES8 引入的,通过简单地“等待”异步操作的执行,在异步函数之上提供了一个新的抽象层。程序流程在该指令处阻塞,直到异步操作返回结果,然后程序将继续执行下一条指令。如果您正在考虑同步执行流程,那么您是对的。我们已经回到原点了! Async/await 尝试将同步编程的简单性带入异步世界。请记住,这只能在程序的执行和代码中感知到。一切都保持不变,异步/等待仍然使用承诺,回调是它们的构建块。

让我们回顾一下我们的示例并使用 Async/await 来实现它:

async function searchOccurrences(word) {
  try {
    // search operation logic, result is in result variable
    //....
    return result;
  } catch (err) {
    console.log(`Search operation for ${word} ended with an error`);
  }
}

const word = "the";

const result = await searchOccurrences(word, handleSearchOccurrencesResult);

console.log(`Search results for ${word}: ${result}`);

我们的代码没有太大变化,这里需要注意的重要一点是 searchOccurrences 函数声明之前的“async”关键字。这表明该函数是异步的。另外,请注意调用 searchOccurrences 函数时的“await”关键字。这将指示程序等待函数的执行,直到返回结果,然后程序才能移动到下一条指令,换句话说,result 变量将始终保存 searchOccurrences 函数的返回值,而不是承诺从这个意义上来说,Async/Await 函数没有 Promises 那样的待处理状态。执行完成后,我们转到打印语句,这次结果实际上包含搜索操作的结果。正如预期的那样,新代码具有与同步代码相同的行为。

另一个需要记住的小事情是,由于我们不再有回调函数,因此我们需要在同一函数内处理 searchOccurrences 错误,因为我们不能将错误传播到回调函数并在那里处理它。为了示例起见,我们只是在出现错误时打印一条错误消息。

## 包起来

在本文中,我们介绍了用于在 Javascript 中实现异步逻辑的不同方法。我们首先探讨了一个具体示例,说明为什么我们需要从常规同步编程风格转变为异步模型。然后我们转向回调,这是异步 Javascript 的主要构建块。回调的局限性导致我们多年来添加了不同的替代方案来克服这些限制,主要是 Promise 和 Async/await。异步逻辑可以在网络上的任何地方找到,无论您是调用外部 API、启动数据库查询、写入本地文件系统,还是等待用户在登录表单上输入。希望您现在更有信心通过编写干净且可维护的异步 Javascript 来解决这些问题!

如果您喜欢这篇文章,请查看 CLA 博客,我们在其中讨论有关如何进入技术领域的各种主题。另外,请查看我们之前的免费研讨会的 youtube 频道,并在 社交媒体 上关注我们-labs-academy/) 这样您就不会错过即将推出的内容!


Career Services background pattern

职业服务

Contact Section background image

让我们保持联系

Code Labs Academy © 2024 版权所有.