如何在Node.js中编写异步代码

The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

作者选择了“ 开放互联网/言论自由基金会”作为“ Write for DOnations”计划的一部分来接受捐赠。

介绍 (Introduction)

For many programs in JavaScript, code is executed as the developer writes it—line by line. This is called synchronous execution, because the lines are executed one after the other, in the order they were written. However, not every instruction you give to the computer needs to be attended to immediately. For example, if you send a network request, the process executing your code will have to wait for the data to return before it can work on it. In this case, time would be wasted if it did not execute other code while waiting for the network request to be completed. To solve this problem, developers use asynchronous programming, in which lines of code are executed in a different order than the one in which they were written. With asynchronous programming, we can execute other code while we wait for long activities like network requests to finish.

对于许多JavaScript程序而言,代码是在开发人员编写代码时逐行执行的。 这被称为同步执行 ,因为这些行按照它们被写入的顺序一个接一个地执行。 但是,并非您提供给计算机的每条指令都需要立即得到照顾。 例如,如果您发送网络请求,则执行代码的进程将必须等待数据返回后才能对其进行处理。 在这种情况下,如果它在等待网络请求完成时不执行其他代码,则会浪费时间。 为了解决此问题,开发人员使用了异步编程 ,在异步编程中 ,代码行的执行顺序与编写代码时的顺序不同。 使用异步编程,我们可以在等待诸如网络请求之类的长时间活动时执行其他代码。

JavaScript code is executed on a single thread within a computer process. Its code is processed synchronously on this thread, with only one instruction run at a time. Therefore, if we were to do a long-running task on this thread, all of the remaining code is blocked until the task is complete. By leveraging JavaScript’s asynchronous programming features, we can offload long-running tasks to a background thread to avoid this problem. When the task is complete, the code we need to process the task’s data is put back on the main single thread.

JavaScript代码在计算机进程内的单个线程上执行。 它的代码在此线程上同步处理,一次仅运行一条指令。 因此,如果我们要在该线程上执行长时间运行的任务,则所有剩余的代码都将被阻塞,直到任务完成为止。 通过利用JavaScript的异步编程功能,我们可以将长时间运行的任务卸载到后台线程中来避免此问题。 当任务完成时,我们需要处理任务数据的代码被放回到主单线程上。

In this tutorial, you will learn how JavaScript manages asynchronous tasks with help from the Event Loop, which is a JavaScript construct that completes a new task while waiting for another. You will then create a program that uses asynchronous programming to request a list of movies from a Studio Ghibli API and save the data to a CSV file. The asynchronous code will be written in three ways: callbacks, promises, and with the async/await keywords.

在本教程中,您将学习JavaScript如何在Event Loop的帮助下管理异步任务, Event Loop是一个JavaScript结构,可以在等待另一个任务的同时完成一个新任务。 然后,您将创建一个程序,该程序使用异步编程来从Studio Ghibli API请求电影列表,并将数据保存到CSV文件 。 异步代码将以三种方式编写:回调,承诺以及带有async / await关键字。

Note: As of this writing, asynchronous programming is no longer done using only callbacks, but learning this obsolete method can provide great context as to why the JavaScript community now uses promises. The async/await keywords enable us to use promises in a less verbose way, and are thus the standard way to do asynchronous programming in JavaScript at the time of writing this article.

注意:在撰写本文时,异步编程不再仅使用回调来完成,而是学习这种过时的方法可以提供一个很好的上下文,以了解JavaScript社区现在为什么使用promises。 async / await关键字使我们能够以不太冗长的方式使用promise,因此是撰写本文时使用JavaScript进行异步编程的标准方法。

先决条件 (Prerequisites)

事件循环 (The Event Loop)

Let’s begin by studying the internal workings of JavaScript function execution. Understanding how this behaves will allow you to write asynchronous code more deliberately, and will help you with troubleshooting code in the future.

让我们开始研究JavaScript函数执行的内部工作原理。 了解它的行为方式将使您能够更加刻意地编写异步代码,并在将来帮助您对代码进行故障排除。

As the JavaScript interpreter executes the code, every function that is called is added to JavaScript’s call stack. The call stack is a stack—a list-like data structure where items can only be added to the top, and removed from the top. Stacks follow the “Last in, first out” or LIFO principle. If you add two items on the stack, the most recently added item is removed first.

当JavaScript解释器执行代码时,每个被调用的函数都会添加到JavaScript的调用栈中 。 调用堆栈是一个堆栈 —一种类似于列表的数据结构,其中的项目只能添加到顶部,也可以从顶部删除。 堆栈遵循“后进先出”或LIFO原则。 如果您在堆栈上添加两个项目,则最先添加的项目将被删除。

Let’s illustrate with an example using the call stack. If JavaScript encounters a function functionA() being called, it is added to the call stack. If that function functionA() calls another function functionB(), then functionB() is added to the top of the call stack. As JavaScript completes the execution of a function, it is removed from the call stack. Therefore, JavaScript will execute functionB() first, remove it from the stack when complete, and then finish the execution of functionA() and remove it from the call stack. This is why inner functions are always executed before their outer functions.

让我们以使用调用堆栈的示例进行说明。 如果JavaScript遇到正在调用的函数functionA() ,则会将其添加到调用堆栈中。 如果该函数functionA()调用另一个函数functionB() ,则将functionB()添加到调用堆栈的顶部。 当JavaScript完成函数的执行时,会将其从调用堆栈中删除。 因此,JavaScript将首先执行functionB() ,完成后将其从堆栈中删除,然后完成functionA()的执行并从调用堆栈中将其删除。 这就是为什么内部函数总是在其外部函数之前执行的原因。

When JavaScript encounters an asynchronous operation, like writing to a file, it adds it to a table in its memory. This table stores the operation, the condition for it to be completed, and the function to be called when it’s completed. As the operation completes, JavaScript adds the associated function to the message queue. A queue is another list-like data structure where items can only be added to the bottom but removed from the top. In the message queue, if two or more asynchronous operations are ready for their functions to be executed, the asynchronous operation that was completed first will have its function marked for execution first.

当JavaScript遇到异步操作(例如写入文件)时,它会将其添加到内存中的表中。 该表存储操作,完成条件以及完成时要调用的函数。 操作完成后,JavaScript将关联的功能添加到消息队列中 。 队列是另一个类似于列表的数据结构,其中项目只能添加到底部,但可以从顶部删除。 在消息队列中,如果两个或多个异步操作准备好执行其功能,则首先完成的异步操作将其功能标记为首先执行。

Functions in the message queue are waiting to be added to the call stack. The event loop is a perpetual process that checks if the call stack is empty. If it is, then the first item in the message queue is moved to the call stack. JavaScript prioritizes functions in the message queue over function calls it interprets in the code. The combined effect of the call stack, message queue, and event loop allows JavaScript code to be processed while managing asynchronous activities.

消息队列中的功能正在等待添加到调用堆栈中。 事件循环是一个永久过程,用于检查调用堆栈是否为空。 如果是,则消息队列中的第一项将移至调用堆栈。 JavaScript将消息队列中的函数优先于其在代码中解释的函数调用。 调用堆栈,消息队列和事件循环的综合作用,可以在管理异步活动的同时处理JavaScript代码。

Now that you have a high-level understanding of the event loop, you know how the asynchronous code you write will be executed. With this knowledge, you can now create asynchronous code with three different approaches: callbacks, promises, and async/await.

现在,您对事件循环有了一个高级的了解,您知道了如何执行编写的异步代码。 有了这些知识,您现在可以使用三种不同的方法创建异步代码:回调,promise和async / await

带回调的异步编程 (Asynchronous Programming with Callbacks)

A callback function is one that is passed as an argument to another function, and then executed when the other function is finished. We use callbacks to ensure that code is executed only after an asynchronous operation is completed.

回调函数是一个作为参数传递给另一个函数,然后在另一个函数完成时执行的函数。 我们使用回调来确保仅在异步操作完成后才执行代码。

For a long time, callbacks were the most common mechanism for writing asynchronous code, but now they have largely become obsolete because they can make code confusing to read. In this step, you’ll write an example of asynchronous code using callbacks so that you can use it as a baseline to see the increased efficiency of other strategies.

长期以来,回调是编写异步代码的最常见机制,但是现在,由于它们使代码难以读取,因此它们已经过时了。 在这一步中,您将编写一个使用回调的异步代码示例,以便您可以将其用作基线来查看其他策略的效率提高。

There are many ways to use callback functions in another function. Generally, they take this structure:

在另一个函数中有多种使用回调函数的方法。 通常,它们采用以下结构:

function asynchronousFunction([ Function Arguments ], [ Callback Function ]) {
    [ Action ]
}

While it is not syntactically required by JavaScript or Node.js to have the callback function as the last argument of the outer function, it is a common practice that makes callbacks easier to identify. It’s also common for JavaScript developers to use an anonymous function as a callback. Anonymous functions are those created without a name. It’s usually much more readable when a function is defined at the end of the argument list.

虽然从语法上讲,JavaScript或Node.js都不要求将回调函数作为外部函数的最后一个参数,但通常的做法是使回调更易于识别。 JavaScript开发人员通常会使用匿名函数作为回调。 匿名函数是那些没有名称的函数。 当在参数列表的末尾定义一个函数时,它通常更具可读性。

To demonstrate callbacks, let’s create a Node.js module that writes a list of Studio Ghibli movies to a file. First, create a folder that will store our JavaScript file and its output:

为了演示回调,让我们创建一个Node.js模块,该模块将Studio Ghibli电影列表写入文件。 首先,创建一个文件夹,该文件夹将存储我们JavaScript文件及其输出:

  • mkdir ghibliMovies

    mkdir ghibli电影

Then enter that folder:

然后输入该文件夹:

  • cd ghibliMovies

    cd ghibli电影

We will start by making an HTTP request to the Studio Ghibli API, which our callback function will log the results of. To do this, we will install a library that allows us to access the data of an HTTP response in a callback.

我们将从向Studio Ghibli API发出HTTP请求开始,我们的回调函数将记录该请求的结果。 为此,我们将安装一个库,该库允许我们在回调中访问HTTP响应的数据。

In your terminal, initialize npm so we can have a reference for our packages later:

在您的终端中,初始化npm,以便稍后我们可以为我们的软件包提供参考:

  • npm init -y

    npm初始化-y

Then, install the request library:

然后,安装request库:

  • npm i request --save

    npm我要求-保存

Now open a new file called callbackMovies.js in a text editor like nano:

现在,在类似于nano的文本编辑器中打开一个名为callbackMovies.js的新文件:

  • nano callbackMovies.js

    纳米callbackMovies.js

In your text editor, enter the following code. Let’s begin by sending an HTTP request with the request module:

在文本编辑器中,输入以下代码。 让我们开始通过request模块发送HTTP请求:

callbackMovies.js
callbackMovies.js
const request = require('request');

request('https://ghibliapi.herokuapp.com/films');

In the first line, we load the request module that was installed via npm. The module returns a function that can make HTTP requests; we then save that function in the request constant.

在第一行中,我们加载通过npm安装的request模块。 该模块返回一个可以发出HTTP请求的函数; 然后,我们将该函数保存在request常量中。

We then make the HTTP request using the request() function. Let’s now print the data from the HTTP request to the console by adding the highlighted changes:

然后,我们使用request()函数request() HTTP请求。 现在,通过添加突出显示的更改,将数据从HTTP请求打印到控制台:

callbackMovies.js
callbackMovies.js
const request = require('request');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    movies.forEach(movie => {
        console.log(`${movie['title']}, ${movie['release_date']}`);
    });
});

When we use the request() function, we give it two parameters:

当我们使用request()函数时,我们给它两个参数:

  • The URL of the website we are trying to request

    我们正在尝试请求的网站的URL
  • A callback function that handles any errors or successful responses after the request is complete

    请求完成后处理任何错误或成功响应的回调函数

Our callback function has three arguments: error, response, and body. When the HTTP request is complete, the arguments are automatically given values depending on the outcome. If the request failed to send, then error would contain an object, but response and body would be null. If it made the request successfully, then the HTTP response is stored in response. If our HTTP response returns data (in this example we get JSON) then the data is set in body.

我们的回调函数具有三个参数: errorresponsebody 。 HTTP请求完成后,将根据结果自动为参数提供值。 如果请求发送失败,则error将包含一个对象,但是responsebodynull 。 如果成功发出了请求,则HTTP响应将存储在response 。 如果我们的HTTP响应返回数据(在本例中为JSON),则将数据设置在body

Our callback function first checks to see if we received an error. It’s best practice to check for errors in a callback first so the execution of the callback won’t continue with missing data. In this case, we log the error and the function’s execution. We then check the status code of the response. Our server may not always be available, and APIs can change causing once sensible requests to become incorrect. By checking that the status code is 200, which means the request was “OK”, we can have confidence that our response is what we expect it to be.

我们的回调函数首先检查是否收到错误。 最佳做法是先检查回调中的错误,以使回调的执行不会因缺少数据而继续。 在这种情况下,我们记录错误和函数的执行。 然后,我们检查响应的状态码。 我们的服务器可能并不总是可用,并且API可能会发生变化,从而导致明智的请求一旦变得不正确。 通过检查状态码为200 ,这意味着请求是“ OK”,我们可以确信我们的响应就是我们期望的结果。

Finally, we parse the response body to an Array and loop through each movie to log its name and release year.

最后,我们将响应主体解析为一个Array并循环遍历每部电影以记录其名称和发行年份。

After saving and quitting the file, run this script with:

保存并退出文件后,使用以下命令运行此脚本:

  • node callbackMovies.js

    节点callbackMovies.js

You will get the following output:

您将获得以下输出:


   
   
Output
Castle in the Sky, 1986 Grave of the Fireflies, 1988 My Neighbor Totoro, 1988 Kiki's Delivery Service, 1989 Only Yesterday, 1991 Porco Rosso, 1992 Pom Poko, 1994 Whisper of the Heart, 1995 Princess Mononoke, 1997 My Neighbors the Yamadas, 1999 Spirited Away, 2001 The Cat Returns, 2002 Howl's Moving Castle, 2004 Tales from Earthsea, 2006 Ponyo, 2008 Arrietty, 2010 From Up on Poppy Hill, 2011 The Wind Rises, 2013 The Tale of the Princess Kaguya, 2013 When Marnie Was There, 2014

We successfully received a list of Studio Ghibli movies with the year they were released. Now let’s complete this program by writing the movie list we are currently logging into a file.

我们成功收到了吉卜力工作室电影发行年份的清单。 现在,通过编写当前正在登录到文件的影片列表来完成该程序。

Update the callbackMovies.js file in your text editor to include the following highlighted code, which creates a CSV file with our movie data:

在文本编辑器中更新callbackMovies.js文件,以包含以下突出显示的代码,该代码将使用我们的电影数据创建CSV文件:

callbackMovies.js
callbackMovies.js
const request = require('request');
const fs = require('fs');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    let movieList = '';
    movies.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });

    fs.writeFile('callbackMovies.csv', movieList, (error) => {
        if (error) {
            console.error(`Could not save the Ghibli movies to a file: ${error}`);
            return;
        }

        console.log('Saved our list of movies to callbackMovies.csv');;
    });
});

Noting the highlighted changes, we see that we import the fs module. This module is standard in all Node.js installations, and it contains a writeFile() method that can asynchronously write to a file.

注意突出显示的更改,我们看到我们导入了fs模块。 该模块在所有Node.js安装中都是标准模块,它包含一个writeFile()方法,该方法可以异步写入文件。

Instead of logging the data to the console, we now add it to a string variable movieList. We then use writeFile() to save the contents of movieList to a new file—callbackMovies.csv. Finally, we provide a callback to the writeFile() function, which has one argument: error. This allows us to handle cases where we are not able to write to a file, for example when the user we are running the node process on does not have those permissions.

现在,我们将数据添加到字符串变量movieList ,而不是将数据记录到控制台。 然后,我们使用writeFile()movieList的内容保存到新文件callbackMovies.csv 。 最后,我们提供了对writeFile()函数的回调,该函数具有一个参数: error 。 这使我们能够处理无法写入文件的情况,例如,当我们在其上运行node进程的用户没有这些权限时。

Save the file and run this Node.js program once again with:

保存文件,然后使用以下命令再次运行此Node.js程序:

  • node callbackMovies.js

    节点callbackMovies.js

In your ghibliMovies folder, you will see callbackMovies.csv, which has the following content:

ghibliMovies文件夹中,您将看到callbackMovies.csv ,其内容如下:

callbackMovies.csv
callbackMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

It’s important to note that we write to our CSV file in the callback of the HTTP request. Once the code is in the callback function, it will only write to the file after the HTTP request was completed. If we wanted to communicate to a database after we wrote our CSV file, we would make another asynchronous function that would be called in the callback of writeFile(). The more asynchronous code we have, the more callback functions have to be nested.

重要的是要注意,我们在HTTP请求的回调中写入了CSV文件。 一旦代码包含在回调函数中,它将仅在HTTP请求完成后才写入文件。 如果我们想在写入CSV文件后与数据库进行通信,我们将创建另一个异步函数,该函数将在writeFile()的回调中调用。 我们拥有的异步代码越多,嵌套的回调函数就越多。

Let’s imagine that we want to execute five asynchronous operations, each one only able to run when another is complete. If we were to code this, we would have something like this:

假设我们要执行五个异步操作,每个异步操作只能在另一个操作完成时才能运行。 如果要编写此代码,则将具有以下内容:

doSomething1(() => {
    doSomething2(() => {
        doSomething3(() => {
            doSomething4(() => {
                doSomething5(() => {
                    // final action
                });
            });
        }); 
    });
});

When nested callbacks have many lines of code to execute, they become substantially more complex and unreadable. As your JavaScript project grows in size and complexity, this effect will become more pronounced, until it is eventually unmanageable. Because of this, developers no longer use callbacks to handle asynchronous operations. To improve the syntax of our asynchronous code, we can use promises instead.

当嵌套的回调有许多行代码要执行时,它们将变得更加复杂且不可读。 随着您JavaScript项目的规模和复杂性的增长,这种影响将变得更加明显,直到最终无法控制。 因此,开发人员不再使用回调来处理异步操作。 为了改善异步代码的语法,我们可以改用promises。

使用承诺进行简洁的异步编程 (Using Promises for Concise Asynchronous Programming)

A promise is a JavaScript object that will return a value at some point in the future. Asynchronous functions can return promise objects instead of concrete values. If we get a value in the future, we say that the promise was fulfilled. If we get an error in the future, we say that the promise was rejected. Otherwise, the promise is still being worked on in a pending state.

promise是一个JavaScript对象,将来会在某个时候返回一个值。 异步函数可以返回promise对象而不是具体值。 如果我们在未来获得价值,则表示承诺已兑现。 如果我们将来遇到错误,我们说诺言被拒绝了。 否则,promise仍处于未决状态。

Promises generally take the following form:

承诺通常采用以下形式:

promiseFunction()
    .then([ Callback Function for Fulfilled Promise ])
    .catch([ Callback Function for Rejected Promise ])

As shown in this template, promises also use callback functions. We have a callback function for the then() method, which is executed when a promise is fulfilled. We also have a callback function for the catch() method to handle any errors that come up while the promise is being executed.

如该模板所示,promise也使用回调函数。 我们为then()方法提供了一个回调函数,该函数在实现诺言时执行。 我们还为catch()方法提供了一个回调函数,以处理执行promise时出现的任何错误。

Let’s get firsthand experience with promises by rewriting our Studio Ghibli program to use promises instead.

让我们通过重写Studio Ghibli程序以使用Promise来获得Promise的第一手经验。

Axios is a promise-based HTTP client for JavaScript, so let’s go ahead and install it:

Axios是用于JavaScript的基于Promise的HTTP客户端,所以让我们继续安装它:

  • npm i axios --save

    npm i axios-保存

Now, with your text editor of choice, create a new file promiseMovies.js:

现在,使用您选择的文本编辑器,创建一个新文件promiseMovies.js

  • nano promiseMovies.js

    纳米promiseMovies.js

Our program will make an HTTP request with axios and then use a special promised-based version of fs to save to a new CSV file.

我们的程序将使用axios发出HTTP请求,然后使用基于承诺的特殊版本的fs保存到新的CSV文件。

Type this code in promiseMovies.js so we can load Axios and send an HTTP request to the movie API:

promiseMovies.js键入以下代码,以便我们可以加载Axios并将HTTP请求发送到电影API:

promiseMovies.js
promiseMovies.js
const axios = require('axios');

axios.get('https://ghibliapi.herokuapp.com/films');

In the first line we load the axios module, storing the returned function in a constant called axios. We then use the axios.get() method to send an HTTP request to the API.

在第一行中,我们加载axios模块,将返回的函数存储在名为axios的常量中。 然后,我们使用axios.get()方法将HTTP请求发送到API。

The axios.get() method returns a promise. Let’s chain that promise so we can print the list of Ghibli movies to the console:

axios.get()方法返回一个axios.get() 。 让我们链接那个承诺,以便我们可以将Ghibli电影列表打印到控制台:

promiseMovies.js
promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        response.data.forEach(movie => {
            console.log(`${movie['title']}, ${movie['release_date']}`);
        });
    })

Let’s break down what’s happening. After making an HTTP GET request with axios.get(), we use the then() function, which is only executed when the promise is fulfilled. In this case, we print the movies to the screen like we did in the callbacks example.

让我们分解正在发生的事情。 在使用axios.get()发出HTTP GET请求之后,我们使用then()函数,该函数仅在实现诺言时执行。 在这种情况下,我们像在回调示例中那样将影片打印到屏幕上。

To improve this program, add the highlighted code to write the HTTP data to a file:

要改进此程序,请添加突出显示的代码以将HTTP数据写入文件:

promiseMovies.js
promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })

We additionally import the fs module once again. Note how after the fs import we have .promises. Node.js includes a promised-based version of the callback-based fs library, so backward compatibility is not broken in legacy projects.

另外,我们再次导入了fs模块。 请注意,在fs导入之后,我们如何拥有.promises 。 Node.js包括基于回调的fs库的基于承诺的版本,因此在旧项目中不会破坏向后兼容性。

The first then() function that processes the HTTP request now calls fs.writeFile() instead of printing to the console. Since we imported the promise-based version of fs, our writeFile() function returns another promise. As such, we append another then() function for when the writeFile() promise is fulfilled.

现在,处理HTTP请求的第一个then()函数将调用fs.writeFile()而不是打印到控制台。 由于我们导入了基于承诺的fs版本,因此我们的writeFile()函数将返回另一个承诺。 这样,当实现writeFile()承诺时,我们将附加另一个then()函数。

A promise can return a new promise, allowing us to execute promises one after the other. This paves the way for us to perform multiple asynchronous operations. This is called promise chaining, and it is analogous to nesting callbacks. The second then() is only called after we successfully write to the file.

一个承诺可以返回一个新的承诺,使我们可以一个接一个地执行承诺。 这为我们执行多个异步操作铺平了道路。 这称为诺言链 ,类似于嵌套回调。 仅在我们成功写入文件后才调用第二个then()

Note: In this example, we did not check for the HTTP status code like we did in the callback example. By default, axios does not fulfil its promise if it gets a status code indicating an error. As such, we no longer need to validate it.

注意:在此示例中,我们没有像在回调示例中那样检查HTTP状态代码。 默认情况下,如果axios收到指示错误的状态代码,则不履行承诺。 因此,我们不再需要对其进行验证。

To complete this program, chain the promise with a catch() function as it is highlighted in the following:

要完成此程序,请使用catch()函数将promise链接起来,如下所示:

promiseMovies.js
promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })
    .catch((error) => {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    });

If any promise is not fulfilled in the chain of promises, JavaScript automatically goes to the catch() function if it was defined. That’s why we only have one catch() clause even though we have two asynchronous operations.

如果承诺链中未实现任何承诺,则JavaScript会自动转到catch()函数(如果已定义)。 这就是为什么即使我们有两个异步操作也只有一个catch()子句的原因。

Let’s confirm that our program produces the same output by running:

让我们确认我们的程序通过运行产生相同的输出:

  • node promiseMovies.js

    节点promiseMovies.js

In your ghibliMovies folder, you will see the promiseMovies.csv file containing:

ghibliMovies文件夹中,您将看到包含以下内容的promiseMovies.csv文件:

promiseMovies.csv
promiseMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

With promises, we can write much more concise code than using only callbacks. The promise chain of callbacks is a cleaner option than nesting callbacks. However, as we make more asynchronous calls, our promise chain becomes longer and harder to maintain.

有了promise,我们可以编写比仅使用回调简单得多的代码。 回调的promise链比嵌套的回调更干净。 但是,随着我们进行更多的异步调用,我们的诺言链变得更长且难以维护。

The verbosity of callbacks and promises come from the need to create functions when we have the result of an asynchronous task. A better experience would be to wait for an asynchronous result and put it in a variable outside the function. That way, we can use the results in the variables without having to make a function. We can achieve this with the async and await keywords.

回调和promise的冗长性来自当我们获得异步任务的结果时需要创建函数。 更好的体验是等待异步结果并将其放入函数外部的变量中。 这样,我们可以在变量中使用结果,而无需创建函数。 我们可以使用asyncawait关键字来实现。

async / await编写JavaScript (Writing JavaScript with async/await)

The async/await keywords provide an alternative syntax when working with promises. Instead of having the result of a promise available in the then() method, the result is returned as a value like in any other function. We define a function with the async keyword to tell JavaScript that it’s an asynchronous function that returns a promise. We use the await keyword to tell JavaScript to return the results of the promise instead of returning the promise itself when it’s fulfilled.

使用诺言时, async / await关键字提供了另一种语法。 而不是在then()方法中提供诺言的结​​果,而是像其他任何函数一样,将结果作为值返回。 我们使用async关键字定义一个函数,以告知JavaScript这是一个返回诺言的异步函数。 我们使用await关键字来告诉JavaScript返回承诺的结果,而不是在实现承诺时返回承诺本身。

In general, async/await usage looks like this:

通常, async / await用法如下所示:

async function() {
    await [Asynchronous Action]
}

Let’s see how using async/await can improve our Studio Ghibli program. Use your text editor to create and open a new file asyncAwaitMovies.js:

让我们看看使用async / await如何改善Studio Ghibli程序。 使用文本编辑器创建并打开一个新文件asyncAwaitMovies.js

  • nano asyncAwaitMovies.js

    纳米asyncAwaitMovies.js

In your newly opened JavaScript file, let’s start by importing the same modules we used in our promise example:

在您新打开JavaScript文件中,让我们开始导入在promise示例中使用的相同模块:

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

The imports are the same as promiseMovies.js because async/await uses promises.

导入与promiseMovies.js相同,因为async / await使用promises。

Now we use the async keyword to create a function with our asynchronous code:

现在,我们使用async关键字通过异步代码创建一个函数:

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {}

We create a new function called saveMovies() but we include async at the beginning of its definition. This is important as we can only use the await keyword in an asynchronous function.

我们创建了一个名为saveMovies()的新函数,但在其定义的开头包含了async 。 这很重要,因为我们只能在异步函数中使用await关键字。

Use the await keyword to make an HTTP request that gets the list of movies from the Ghibli API:

使用await关键字发出HTTP请求,该请求从Ghibli API获取电影列表:

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
}

In our saveMovies() function, we make an HTTP request with axios.get() like before. This time, we don’t chain it with a then() function. Instead, we add await before it is called. When JavaScript sees await, it will only execute the remaining code of the function after axios.get() finishes execution and sets the response variable. The other code saves the movie data so we can write to a file.

在我们的saveMovies()函数中,我们像以前一样通过axios.get()发出HTTP请求。 这次,我们不使用then()函数将其链接。 相反,我们在调用之前添加了一个await 。 当JavaScript看到await ,它将仅在axios.get()完成执行并设置response变量之后执行该函数的其余代码。 其他代码保存电影数据,以便我们可以写入文件。

Let’s write the movie data to a file:

让我们将影片数据写入文件:

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
    await fs.writeFile('asyncAwaitMovies.csv', movieList);
}

We also use the await keyword when we write to the file with fs.writeFile().

当我们使用fs.writeFile()写入文件时,我们也使用await关键字。

To complete this function, we need to catch errors our promises can throw. Let’s do this by encapsulating our code in a try/catch block:

要完成此功能,我们需要捕获承诺可能引发的错误。 通过将我们的代码封装在try / catch块中,可以做到这一点:

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

Since promises can fail, we encase our asynchronous code with a try/catch clause. This will capture any errors that are thrown when either the HTTP request or file writing operations fail.

由于Promise可能失败,因此我们用try / catch子句来封装异步代码。 这将捕获HTTP请求或文件写入操作失败时引发的所有错误。

Finally, let’s call our asynchronous function saveMovies() so it will be executed when we run the program with node

最后,让我们调用异步函数saveMovies()以便在使用node运行程序时执行该异步函数。

asyncAwaitMovies.js
asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

saveMovies();

At a glance, this looks like a typical synchronous JavaScript code block. It has fewer functions being passed around, which looks a bit neater. These small tweaks make asynchronous code with async/await easier to maintain.

乍一看,这看起来像一个典型的同步JavaScript代码块。 它传递的功能较少,看起来更整洁。 这些小的调整使带有async / await异步代码更易于维护。

Test this iteration of our program by entering this in your terminal:

通过在终端中输入以下内容来测试我们程序的迭代:

  • node asyncAwaitMovies.js

    节点asyncAwaitMovies.js

In your ghibliMovies folder, a new asyncAwaitMovies.csv file will be created with the following contents:

在您的ghibliMovies文件夹中,将创建一个新的asyncAwaitMovies.csv文件,其中包含以下内容:

asyncAwaitMovies.csv
asyncAwaitMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

You have now used the JavaScript features async/await to manage asynchronous code.

现在,您已使用JavaScript功能async / await来管理异步代码。

结论 (Conclusion)

In this tutorial, you learned how JavaScript handles executing functions and managing asynchronous operations with the event loop. You then wrote programs that created a CSV file after making an HTTP request for movie data using various asynchronous programming techniques. First, you used the obsolete callback-based approach. You then used promises, and finally async/await to make the promise syntax more succinct.

在本教程中,您学习了JavaScript如何通过事件循环处理执行函数和管理异步操作。 然后,您编写了使用各种异步编程技术对电影数据发出HTTP请求后创建CSV文件的程序。 首先,您使用了过时的基于回调的方法。 然后,您使用了Promise,最后async / await以使Promise语法更加简洁。

With your understanding of asynchronous code with Node.js, you can now develop programs that benefit from asynchronous programming, like those that rely on API calls. Have a look at this list of public APIs. To use them, you will have to make asynchronous HTTP requests like we did in this tutorial. For further study, try building an app that uses these APIs to practice the techniques you learned here.

了解了Node.js的异步代码后,您现在可以开发受益于异步编程的程序,例如依赖API调用的程序。 看看这个公共API列表。 要使用它们,您将必须像本教程中一样进行异步HTTP请求。 为了进一步研究,请尝试构建一个使用这些API来实践您在此处学到的技术的应用。

翻译自: https://www.digitalocean.com/community/tutorials/how-to-write-asynchronous-code-in-node-js

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值