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

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

对于许多JavaScript程序来说,代码是按照开发者编写的方式执行的――一行一行。这被称为同步执行,因为这些行是按照编写的顺序执行的。但是,你给计算机的每一个指令都不需要立即执行。例如,如果你发送网络请求,执行代码的过程必须等待数据返回,然后它可以工作。在这种情况下,如果它在等待网络请求完成时不执行其他代码,那么时间就会浪费。

JavaScript 代码在计算机过程中的单个线程上执行,它的代码在这个线程上进行同步处理,一次只运行一个指令,因此,如果我们要在这个线程上执行一个长时间的任务,所有剩余的代码都被阻止,直到任务完成。

在本教程中,您将学习 JavaScript 如何使用 Event Loop 来管理非同步任务,这是一个 JavaScript 构造,在等待另一个任务时完成一个新的任务,然后您将创建一个程序,该程序使用非同步编程从 Studio Ghibli API请求电影列表,并将数据保存到 CSV 文件

<$>[note] **注:**从本文写作开始,非同步编程不再只使用回复,但学习这种过时的方法可以为为什么JavaScript社区现在使用承诺提供很好的背景。

前提条件

活动路径

首先,我们来研究JavaScript函数执行的内部运作方式,了解这种运作方式将使您能够更有意地编写非同步代码,并在未来帮助您解决代码问题。

当JavaScript解释器执行代码时,呼叫的每个函数都被添加到JavaScript的 call stack。呼叫堆是 stack - 一个类似列表的数据结构,其中项目只能添加到顶部,然后从顶部删除。

如果JavaScript遇到一个被调用的函数functionA(),它会被添加到调用函数中。如果该函数functionA()会调用另一个函数functionB(),那么functionB()会被添加到调用函数的顶部。当JavaScript完成执行函数时,它会从调用函数中移除。因此,JavaScript会首先执行functionB(),完成时将其从堆栈中移除,然后完成functionA()的执行,并从调用函数中移除。

当JavaScript遇到非同步操作时,例如写到文件时,它会将其添加到其内存中的表格中。此表格存储该操作,该操作的条件,以及在完成时要调用的函数。随着操作的完成,JavaScript会将相关函数添加到 message queue。 排列是另一个类似列表的数据结构,其中项目只能添加到底部,但可以从顶部删除。

消息排列中的函数正在等待被添加到呼叫排列中。 event loop 是一个连续的过程,它检查呼叫排列是否空. 如果是,则消息排列中的第一个项目被移动到呼叫排列中。

现在你对事件循环有了高层次的理解,你知道你写的非同步代码将如何执行. 有了这些知识,你现在可以用三种不同的方法创建非同步代码:回调,承诺和‘async’/‘await’。

非同步编程与召回

_callback 函数是作为一个参数传递给另一个函数,然后在另一个函数完成后执行的函数。

很长一段时间以来,回复是编写非同步代码的最常见机制,但现在它们大多已经过时,因为它们可以使代码难以阅读. 在此步骤中,您将使用回复代码编写一个非同步代码的例子,以便您可以将其用作基线,以查看其他策略的提高效率。

在另一个函数中使用回调函数的方法有很多,一般来说,它们采用了这样的结构:

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

虽然 JavaScript 或 Node.js 没有语法要求将调用函数作为外部函数的最后一个论点,但这是一个常见的做法,使调用函数更容易识别。

要演示回应,让我们创建一个 Node.js 模块,将 Studio Ghibli电影列表写入一个文件中。

1mkdir ghibliMovies

然后输入这个文件夹:

1cd ghibliMovies

首先,我们将向Studio Ghibli API(https://ghibliapi.herokuapp.com/)发送HTTP请求,我们的回复函数会记录结果,这样做时,我们将安装一个库,允许我们在回复中访问HTTP响应的数据。

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

1npm init -y

於是,就把圖書館放進去吧。

1npm i request --save

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

1nano callbackMovies.js

在您的文本编辑器中,输入以下代码,让我们先用请求模块发送 HTTP 请求:

1[label callbackMovies.js]
2const request = require('request');
3
4request('https://ghibliapi.herokuapp.com/films');

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

然后,我们使用请求()函数创建 HTTP 请求,现在让我们将 HTTP 请求的数据打印到控制台,添加所突出的更改:

 1[label callbackMovies.js]
 2const request = require('request');
 3
 4request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
 5    if (error) {
 6        console.error(`Could not send request to API: ${error.message}`);
 7        return;
 8    }
 9
10    if (response.statusCode != 200) {
11        console.error(`Expected status code 200 but received ${response.statusCode}.`);
12        return;
13    }
14
15    console.log('Processing our list of movies');
16    movies = JSON.parse(body);
17    movies.forEach(movie => {
18        console.log(`${movie['title']}, ${movie['release_date']}`);
19    });
20});

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

*我们试图请求的网站的URL

  • 回复函数,在请求完成后处理任何错误或成功回复

我们的回复函数有三个参数:错误,响应身体。当 HTTP 请求完成时,参数会根据结果自动给出值。如果请求未能发送,那么错误将包含一个对象,但响应身体将是无效

我们的回复功能首先检查我们是否收到错误。 最好的做法是先检查回复中的错误,以便回复的执行不会继续与缺失的数据。 在这种情况下,我们记录错误和函数的执行。 然后我们检查响应的状态代码。 我们的服务器可能并不总是可用,并且API可能会改变,导致一次合理的请求变得不正确。 通过检查状态代码为200,这意味着请求是OK,我们可以有信心我们的响应是我们期望的。

最后,我们将响应体解析到一个阵列,并通过每个电影记录其名称和发行年份。

保存和离开文件后,运行此脚本:

1node callbackMovies.js

你会得到以下的输出:

 1[secondary_label Output]
 2Castle in the Sky, 1986
 3Grave of the Fireflies, 1988
 4My Neighbor Totoro, 1988
 5Kiki's Delivery Service, 1989
 6Only Yesterday, 1991
 7Porco Rosso, 1992
 8Pom Poko, 1994
 9Whisper of the Heart, 1995
10Princess Mononoke, 1997
11My Neighbors the Yamadas, 1999
12Spirited Away, 2001
13The Cat Returns, 2002
14Howl's Moving Castle, 2004
15Tales from Earthsea, 2006
16Ponyo, 2008
17Arrietty, 2010
18From Up on Poppy Hill, 2011
19The Wind Rises, 2013
20The Tale of the Princess Kaguya, 2013
21When Marnie Was There, 2014

我們成功地收到了一個列表Studio Ghibli電影的年份,他們發行了. 現在讓我們完成這個程序,寫下電影列表,我們目前正在登入一個檔案。

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

 1[label callbackMovies.js]
 2const request = require('request');
 3const fs = require('fs');
 4
 5request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
 6    if (error) {
 7        console.error(`Could not send request to API: ${error.message}`);
 8        return;
 9    }
10
11    if (response.statusCode != 200) {
12        console.error(`Expected status code 200 but received ${response.statusCode}.`);
13        return;
14    }
15
16    console.log('Processing our list of movies');
17    movies = JSON.parse(body);
18    let movieList = '';
19    movies.forEach(movie => {
20        movieList += `${movie['title']}, ${movie['release_date']}\n`;
21    });
22
23    fs.writeFile('callbackMovies.csv', movieList, (error) => {
24        if (error) {
25            console.error(`Could not save the Ghibli movies to a file: ${error}`);
26            return;
27        }
28
29        console.log('Saved our list of movies to callbackMovies.csv');;
30    });
31});

注意到所突出的变化,我们看到我们导入了fs模块. 该模块在所有 Node.js 安装中都是标准的,并且包含一个writeFile()方法,可以无同步写入文件。

然后,我们使用writeFile()movieList的内容保存到一个新的文件中,即callbackMovies.csv。最后,我们向writeFile()函数提供回调,该函数有一个论点:错误

保存文件并再次运行此 Node.js 程序:

1node callbackMovies.js

在你的ghibliMovies文件夹中,你会看到callbackMovies.csv,其内容如下:

 1[label callbackMovies.csv]
 2Castle in the Sky, 1986
 3Grave of the Fireflies, 1988
 4My Neighbor Totoro, 1988
 5Kiki's Delivery Service, 1989
 6Only Yesterday, 1991
 7Porco Rosso, 1992
 8Pom Poko, 1994
 9Whisper of the Heart, 1995
10Princess Mononoke, 1997
11My Neighbors the Yamadas, 1999
12Spirited Away, 2001
13The Cat Returns, 2002
14Howl's Moving Castle, 2004
15Tales from Earthsea, 2006
16Ponyo, 2008
17Arrietty, 2010
18From Up on Poppy Hill, 2011
19The Wind Rises, 2013
20The Tale of the Princess Kaguya, 2013
21When Marnie Was There, 2014

重要的是要注意,我们在HTTP请求的召回中写到我们的CSV文件,一旦代码处于召回函数中,它只会在HTTP请求完成后写到文件中。如果我们在写下CSV文件后想与数据库进行通信,我们会创建另一个非同步函数,在召回中将被称为writeFile()

让我们想象一下,我们想要执行五个不同步操作,每个操作只有在另一个操作完成时才能运行。

 1doSomething1(() => {
 2    doSomething2(() => {
 3        doSomething3(() => {
 4            doSomething4(() => {
 5                doSomething5(() => {
 6                    // final action
 7                });
 8            });
 9        }); 
10    });
11});

当嵌入式回复有许多行代码来执行时,它们会变得更复杂和不可读。随着您的JavaScript项目的规模和复杂性增加,这种效果将变得更加明显,直到它最终无法管理。

使用承诺进行简洁的非同步编程

一个 promise 是一个 JavaScript 对象,将在未来某个时候返回一个值。 无同步函数可以返回承诺对象而不是具体值。 如果我们在未来获得一个值,我们会说承诺已经实现了。

承诺一般采取如下形式:

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

正如本模板所示,承诺还使用回调函数. 我们有一个回调函数用于然后()方法,该函数在实现承诺时执行。

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

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

1npm i axios --save

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

1nano promiseMovies.js

我们的程序会用axios进行HTTP请求,然后使用特殊的fs版本来保存到新的CSV文件中。

在promiseMovies.js中输入此代码,以便我们可以加载Axios并向电影API发送HTTP请求:

1[label promiseMovies.js]
2const axios = require('axios');
3
4axios.get('https://ghibliapi.herokuapp.com/films');

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

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

 1[label promiseMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5axios.get('https://ghibliapi.herokuapp.com/films')
 6    .then((response) => {
 7        console.log('Successfully retrieved our list of movies');
 8        response.data.forEach(movie => {
 9            console.log(`${movie['title']}, ${movie['release_date']}`);
10        });
11    })

让我们分解一下发生了什么事. 在使用 axios.get() 进行 HTTP GET 请求后,我们使用了 then() 函数,该函数仅在承诺实现时执行。

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

 1[label promiseMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5axios.get('https://ghibliapi.herokuapp.com/films')
 6    .then((response) => {
 7        console.log('Successfully retrieved our list of movies');
 8        let movieList = '';
 9        response.data.forEach(movie => {
10            movieList += `${movie['title']}, ${movie['release_date']}\n`;
11        });
12
13        return fs.writeFile('promiseMovies.csv', movieList);
14    })
15    .then(() => {
16        console.log('Saved our list of movies to promiseMovies.csv');
17    })

我们还将再次导入fs模块,请注意在fs导入后,我们将有.promises。Node.js 包含了基于回调的fs库的基于承诺的版本,因此在传统项目中,反向兼容性不会被破坏。

处理 HTTP 请求的第一个 then() 函数现在呼叫 fs.writeFile(),而不是打印到控制台. 由于我们导入了 fs 的基于承诺的版本,我们的 writeFile() 函数返回另一个承诺。

一个承诺可以返回一个新的承诺,使我们能够执行一个接一个的承诺,这为我们开辟了执行多个不同步操作的道路,这被称为 promise chaining,它类似于嵌套回调。

<$>[注] 注: 在本示例中,我们没有像在回复示例中一样检查HTTP状态代码。 默认情况下,如果它收到一个表示错误的状态代码,‘axios’不会兑现其承诺。

要完成此程序,请将承诺链接到一个 catch() 函数,如下所示:

 1[label promiseMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5axios.get('https://ghibliapi.herokuapp.com/films')
 6    .then((response) => {
 7        console.log('Successfully retrieved our list of movies');
 8        let movieList = '';
 9        response.data.forEach(movie => {
10            movieList += `${movie['title']}, ${movie['release_date']}\n`;
11        });
12
13        return fs.writeFile('promiseMovies.csv', movieList);
14    })
15    .then(() => {
16        console.log('Saved our list of movies to promiseMovies.csv');
17    })
18    .catch((error) => {
19        console.error(`Could not save the Ghibli movies to a file: ${error}`);
20    });

如果在承诺链中没有实现任何承诺,JavaScript会自动转到catch()函数,如果它被定义,这就是为什么我们只有一个catch()条款,即使我们有两个不同步操作。

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

1node promiseMovies.js

在您的ghibliMovies文件夹中,您将看到包含的promiseMovies.csv文件:

 1[label promiseMovies.csv]
 2Castle in the Sky, 1986
 3Grave of the Fireflies, 1988
 4My Neighbor Totoro, 1988
 5Kiki's Delivery Service, 1989
 6Only Yesterday, 1991
 7Porco Rosso, 1992
 8Pom Poko, 1994
 9Whisper of the Heart, 1995
10Princess Mononoke, 1997
11My Neighbors the Yamadas, 1999
12Spirited Away, 2001
13The Cat Returns, 2002
14Howl's Moving Castle, 2004
15Tales from Earthsea, 2006
16Ponyo, 2008
17Arrietty, 2010
18From Up on Poppy Hill, 2011
19The Wind Rises, 2013
20The Tale of the Princess Kaguya, 2013
21When Marnie Was There, 2014

借助承诺,我们可以写出比仅使用回调更简洁的代码。回调的承诺链是一个更干净的选择,而不是固定回调。

反馈和承诺的可言性来自于在我们有非同步任务的结果时需要创建函数。一个更好的体验是等待非同步结果并将其放入函数之外的变量中。

使用AsyncWait编写 JavaScript

async / Wait 关键字在使用承诺时提供替代语法,而不是在 then() 方法中提供承诺的结果,结果会像在任何其他函数中一样返回值。我们定义一个函数使用 async 关键字来告诉 JavaScript 这是一个返回承诺的非同步函数。

一般而言,async/Wait的使用方式看起来如下:

1async function() {
2    await [Asynchronous Action]
3}

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

1nano asyncAwaitMovies.js

在你刚刚打开的JavaScript文件中,让我们先导入我们在我们的示例中使用的相同模块:

1[label asyncAwaitMovies.js]
2const axios = require('axios');
3const fs = require('fs').promises;

导入与promiseMovies.js相同,因为async使用了许诺。

现在我们使用async的关键字来创建一个函数与我们的非同步代码:

1[label asyncAwaitMovies.js]
2const axios = require('axios');
3const fs = require('fs').promises;
4
5async function saveMovies() {}

我们创建一个名为saveMovies()的新函数,但我们在其定义的开始时包括async

使用等待关键字来创建一个 HTTP 请求,从 Ghibli API 获取电影列表:

 1[label asyncAwaitMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5async function saveMovies() {
 6    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
 7    let movieList = '';
 8    response.data.forEach(movie => {
 9        movieList += `${movie['title']}, ${movie['release_date']}\n`;
10    });
11}

在我们的saveMovies()函数中,我们用axios.get()进行HTTP请求,就像以前一样。这次,我们不会将其链接到then()函数中。相反,我们在呼叫之前添加Wait。当JavaScript看到Wait时,它只会在axios.get()完成执行后执行函数的剩余代码并设置响应变量。

让我们把电影数据写成一个文件:

 1[label asyncAwaitMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5async function saveMovies() {
 6    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
 7    let movieList = '';
 8    response.data.forEach(movie => {
 9        movieList += `${movie['title']}, ${movie['release_date']}\n`;
10    });
11    await fs.writeFile('asyncAwaitMovies.csv', movieList);
12}

我们还使用等待的关键字,当我们用fs.writeFile()来写到文件。

要完成此功能,我们需要捕捉我们的承诺可以扔的错误,让我们通过将我们的代码嵌入到一个试用/捕捉块中:

 1[label asyncAwaitMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5async function saveMovies() {
 6    try {
 7        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
 8        let movieList = '';
 9        response.data.forEach(movie => {
10            movieList += `${movie['title']}, ${movie['release_date']}\n`;
11        });
12        await fs.writeFile('asyncAwaitMovies.csv', movieList);
13    } catch (error) {
14        console.error(`Could not save the Ghibli movies to a file: ${error}`);
15    }
16}

由于承诺可能会失败,我们将我们的非同步代码嵌入一个试用捕捉条款,这将捕捉当HTTP请求或文件编写操作失败时丢失的任何错误。

最后,让我们将我们的非同步函数称为saveMovies(),所以当我们使用节点运行程序时,它将执行。

 1[label asyncAwaitMovies.js]
 2const axios = require('axios');
 3const fs = require('fs').promises;
 4
 5async function saveMovies() {
 6    try {
 7        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
 8        let movieList = '';
 9        response.data.forEach(movie => {
10            movieList += `${movie['title']}, ${movie['release_date']}\n`;
11        });
12        await fs.writeFile('asyncAwaitMovies.csv', movieList);
13    } catch (error) {
14        console.error(`Could not save the Ghibli movies to a file: ${error}`);
15    }
16}
17
18saveMovies();

一眼看,这看起来像一个典型的同步JavaScript代码块。它有更少的功能被传递,这看起来有点小。

通过输入到您的终端来测试我们的程序的这个迭代:

1node asyncAwaitMovies.js

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

 1[label asyncAwaitMovies.csv]
 2Castle in the Sky, 1986
 3Grave of the Fireflies, 1988
 4My Neighbor Totoro, 1988
 5Kiki's Delivery Service, 1989
 6Only Yesterday, 1991
 7Porco Rosso, 1992
 8Pom Poko, 1994
 9Whisper of the Heart, 1995
10Princess Mononoke, 1997
11My Neighbors the Yamadas, 1999
12Spirited Away, 2001
13The Cat Returns, 2002
14Howl's Moving Castle, 2004
15Tales from Earthsea, 2006
16Ponyo, 2008
17Arrietty, 2010
18From Up on Poppy Hill, 2011
19The Wind Rises, 2013
20The Tale of the Princess Kaguya, 2013
21When Marnie Was There, 2014

您现在已经使用了 JavaScript 功能 async/ Wait 来管理非同步代码。

结论

在本教程中,您了解了 JavaScript 如何处理执行函数并管理与事件循环的无同步操作,然后您在使用各种无同步编程技术对电影数据提出 HTTP 请求后编写了创建 CSV 文件的程序。

有了您对 Node.js 的非同步代码的了解,您现在可以开发受益于非同步编程的程序,例如那些依赖于 API 调用的程序。 查看此列表的 公共 API)。 要使用它们,您将需要像我们在本教程中所做的那样进行非同步 HTTP 请求。

Published At
Categories with 技术
comments powered by Disqus