java 异步得到函数返回值_使用JavaScript进行异步编程

80242f9b21d89266d4632b9f5dcde158.png

毫无疑问,虽然JavaScript的历史比较悠久,但这并不妨碍它成为当今最受欢迎的编程语言之一。对刚接触该语言的人来说,JavaScript的异步特性可能会有一些挑战。在本文中,我们将了解和使用Promiseasync/await来编写小型异步程序。通过这些示例,你将了解一些可以在自己程序中使用的异步技巧。

本文中的所有代码示例都是基于Node环境编写的,因此建议安装Node以后运行。虽然所有程序都是为Node编写的,但类似的语法在浏览器中也能同样运行,它们的异步编程的写法和原理是通用的。

序言

不管你是否相信JavaScript是一门真正的编程语言,事实是它现在非常的流行。如果你是Web开发人员,你就更应该花一些时间来学习它的优缺点。

JavaScript是单线程的,并且相当于是非阻塞异步流。如果是刚开始使用JavaScript进行异步编程,那么在调试异步代码时,可能会产生很多烦恼。相比常见的同步编程,异步编程需要更多的耐心和不同的思维方式。

在同步模式中,所有操作都发生在一个队列(或者中,更易于对程序进行推理;但是在异步模式中,操作可以在任何时间点以任何顺序开始或结束,每个函数执行结束的时间是不可预测。因此,仅仅依靠运行的顺序序列是不够的。异步编程需要在程序流程和设计方面进行更多思考。

在本文中,我们会尝试几个简单的异步程序,从简单到复杂。我们将编写实现这两个场景功能:

  • 将文件内容写入另一个文件。
  • 将多个文件的内容写入新文件。

Promises 和 async/await

让我们花点时间快速回顾一下promise和async / await的基础知识。

Promises

  • Promise是代表异步操作结果的对象。

  • Promise上有两个回调:resolve(成功之后的回调函数)reject(失败后的回调函数)

  • 一般而言,resolve的结果可以通过then获取。而reject的结果可以通过catch来获取。

  • 可以通过new关键字来使用Promise构造函数创建Promise。例如:

    const p = new Promise((r, j) => {});

  • 这里r回调在 resolve时调用,j回调在reject时调用。

另外Promise对象有一些实用的静态方法如allraceresolvereject

  • all方法可以将多个Promise实例包装成一个新的Promise实例,全部的Promiseresolve的时候返回的是一个结果数组,有任何reject都会使最后的结果变为reject
  • race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是resolve状态还是reject状态。
  • resolve方法创建一个Promise实例并调用resolve方法处理给定参数。
  • reject方法创建一个Promise实例并调用reject方法处理给定参数。

async/await

  • async/await的目的是简化同时使用多个Promise时的行为,避免了大量使用回调(而带来的回调地狱)。
  • 正如Promise类似于结构化回调,async/await结合了生成器Promise的特点简化了异步程序的编写。
  • 可以使用async关键字将函数标记为异步函数。即:async function hello() {}const hello = async() => {};
  • async函数返回的永远是一个Promise对象。只要是async函数的返回值,必然会被包含在Promise对象中。
  • 如果async函数内部存在未捕捉到的异常,则通过Promisereject返回异常。
  • 可以在async函数内部返回Promise对象的语句前使用await 。这种情况下,函数的执行将被“暂停”,直到awaitPromise语句执行完毕,并且返回值不再是一个Promise对象而是其resolve的返回结果。
  • await只在async方法内部的有效。

读写单个文件

本节中,我们将编写一个脚本来读取单个文件的内容,并将其写入一个新文件。

首先,我们将为程序的入口创建一个async方法:

async 

然后,我们需要创建两个Promise,一个代表文件的内容,另一个代表将内容写入另一个文件的操作结果:

async  

在上面的代码段中,readFilewriteFile都是异步的,并且都返回一个Promise。因此,需要使用await来确保readFile有返回值,以便在writeFile中使用它:

async  

最后,可以考虑一下要在main函数中返回什么。在这里,我们打算返回要写入的新文件的名称。要注意的是,返回值将被自动包装在Promise对象中。但是我们需要使用await来确保在函数执行完之前得到了writeFile的结果:

async  

现在,我们可以调用main函数并将结果或任何未捕获的异常打印出来:

main()
.then(r => console.log("Result:", r))
.catch(err => console.log("An error occurred", err);

为了使程序更加完整,我们需要使用fs模块并将fs.readFilefs.writeFilePromise化,即promisify。完整的脚本如下所示:

const util = 

在上面的代码段中,我们Promise化了fs.writeFilefs.readFile。promisify函数可以将任何遵循Node.js回调风格的函数,转换为基于Promise的函数。

接下来我们聊聊异常处理。你可以通过好几种方法来处理异常,具体取决于你想处理到什么程度。例如,在上面的代码片段中,我们在catch块里基本上捕获了main函数中可能发生的任何异常。不过它只在async方法内有用,未捕获的异常会通过该函数的reject返回。

但是,假设你想做更多的控制,并且希望根据每个async方法的错误来做不同的操作。在这种情况下,你可以在每个异步操作中使用try-catchcatch

使用try-catch的情况

我们先来看一下用try-catch的情况。

async  

在上面的代码段中,我们添加了两个try-catch块。另外,我们在第一个程序块try-catch之外创建了fileContent变量,以便在整个main函数中可用。注意,在每个try-catch中,如果出现异常,我们返回的是一个对象。错误对象包含一个消息字段和错误的详细信息。如果发生任何错误,函数会立即返回我们自定义的错误对象。请记住,返回的对象会被自动包含在Promise中。我们可以像以前一样调用main函数,不过这次可以在then()中检查错误对象:

main()
.then(r => {
if(r.error) {
return console.log(
"An error occurred, recover here. Details:", r);
}
return console.log("Done, no error. Result:", r);
})
.catch(err => console.log("An error occurred", err));

注意,在then()中,我们会检查resolve对象是否存在错误。如果有,那么我们在这里进行错误处理;否则,我们只需将结果打印到日志。另一个catch块将捕获运行时错误或程序未处理的其他错误。

使用catch的情况

除了try-catch,我们也可以通过给每个Promise绑定一个catch来处理异常:

async  

这里你可能注意到了,我们给每个Promise 都加了catch方法,并返回一个自定义错误对象,类似于前面的示例。如果其中一个步骤有错误,将只返回这一步的结果,该结果仅包含我们的自定义错误对象。

但是,对于第二个操作,如果写操作成功,我们将明确地返回一个空对象。这是因为writeFile操作成功时传递给resolve的是undefined,而我们无法访问undefined值的error字段。所以如果写入成功,我们要返回一个resolve空对象的Promise

我们还可以写两个辅助函数,减少一些重复代码:

const call = 

call函数接受一个Promise,并返回一个Promise。如果结果为null或未定义,Promise将使用空对象进行处理;或者是操作的结果。如果有错误,将解析为一个包含错误信息的error对象。

error辅助函数需要result和msg两个参数,它将返回包含错误结果和自定义消息的对象。

添加这两个函数后,我们可以更新main函数:

async  

这里,我们将每个操作传递给call函数。然后检查是否有错误,如果有,那么只需调用error函数以返回带有自定义错误消息的自定义错误。完整的代码如下所示:

const util = 

为了更多地减少重复代码并使它变得更加模块化,我们还可以做两件事:

  • 使用fs-extra并删除所有对util.promisify的调用。
  • 将这两个辅助函数放到它们自己的文件中。

之后,我们将得到以下内容:

const fs = 

注意,由于我们正在使用fs-extra,如果不将回调传递给方法,则该函数默认将返回一个Promise。这就是为什么我们删除所有promisify调用,并直接在fs变量上转换所有fs调用的原因。另外,我们将两个辅助函数放到了他们自己的call.js文件中。

读写多个文件

在本节中,我们将编写一个脚本,该脚本读取多个文件的内容并将结果写入新文件。此示例的设置与上一节非常相似:

const fs = 

在上面的代码段中,首先我们需要fs-extra具有所有基于Promise的方法版本的模块fs。然后,我们将main async函数定义为程序的入口点。我们还定义了一个数组,其中包含要读取的文件的硬编码路径。

接下来,我们将编写一个遍历文件路径的for循环,并读取每个文件的内容:

const fs = 

在A行上,我们定义了for循环。在B行await上,我们根据的结果,fs.readFile并将其分配给content变量。最后,在C行中,我们将内容记录到控制台。让我们用实际的写文件操作替换log语句:

const fs = 

在上面的代码段中,我们首先在A行中定义文件的路径。然后在B行中,将结果写入新路径,并确保await在其上也是如此。我们需要在await这里,因为我们要确保在移至下一个文件之前完成写入。最后在C行,我们返回输入文件路径。

现在,上面的实现还可以,但是我们可以做得更好。在上面的实现中,我们一次处理一个文件。也就是说,我们等待每个文件的读写操作完成,然后再移动到下一个文件。实际上,我们可以通过创建一个Promise数组并发地运行每个读写过程,其中的每个Promise表示对文件的读写操作。最后,我们可以用来Promise.all(Promise[])方法同时处理所有Promise

const fs = 

在上面的代码段中,我们在A行上定义了一个数组来保存读写Promise。在行B上,我们开始遍历每个文件路径的for循环。在C行上,我们将自调用async函数推入readWrites数组。在每个async函数的主体内,我们读取每个文件的内容并写入一个新文件。在F行上,我们返回的结果fs.writeFile是一个Promise对象。最后,在G行中,我们用于Promise.all同时处理所有Promise。我们还await对结果进行解析,该结果解析为保存写入结果的单个数组。如果写操作成功,我们应该得到一个未定义值的数组。这是因为write方法解析为undefined没有发生错误。

即使上面的实现完成了工作,我们也可以做得更好。我们可以在files数组上使用带有async函数的map方法,而无需使用自调用async函数。它也将更容易理解:

const fs = 

在上面的代码的A行中我们对files数组执行map方法,把它传递给一个async函数。在async函数内部,我们仅执行读写操作。最后在D行,我们调用Promise.all并传递readWrites数组。该readWrites数组保存了多个Promise,其中每个Promise代表每次读取和写入的结果。

现在,让我们扩展上面的示例。让我们创建一个文件夹,并将所有新文件放入其中。async在进行读写操作之前,我们将需要创建一个函数来为我们创建输出文件夹:

async  

在上面的代码段中,我们首先创建一个async名为的函数prepare。在A行,首先,output如果文件夹已经存在,则将其删除。我们还等待Promise完成,然后再移至B行。在B行上,我们创建了output文件夹,我们也等待完成。现在,在开始读写操作之前,我们可以在prepare函数内部使用该函数main

const fs = 

在A行上,我们等待prepare函数完成,然后再进行读写操作。我们还在行B上更新了输出文件路径。脚本的其余部分几乎相同。我们还将filesand output变量移到了main函数之外。如果运行上面的脚本,应该会看到一个output包含每个输入文件副本的文件夹。

结论

JavaScript从诞生到现在,已经演化为一个非常先进易用的语言,并且Promise以及async/await使异步程序变得更易写也更易读。现在我们已经到了文章的结尾,让我们回顾一些其它的要点:

  • 我们可以Promise.all与数组的map方法一起使用来创建Promise并同时处理它们。我们也可以在Promise.all等待所有Promise被完成之前使用await运算符,即:await Promise.all(inputs.map(async v => {}));
  • 如果要在async函数内部使用try-catch块,则需要在返回Promise的任何Promise值或函数之前使用await运算符。

JavaScript是一个功能强大的全栈语言,不仅可以开发Web前端,也使用Node.js开发后端,使用Electron开发桌面应用。同时也可以结合CukeTest、LeanRunner等工具开发自动化测试及RPA,应了那句老话"学好JavaScript,走遍天下都不怕"。学好异步编程是掌握JavaScript的关键,希望这篇文章对你有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值