Node.js手册,第7部分:异步编程

今天,在翻译Node.js手册的第七部分时,我们将讨论异步编程,考虑使用回调,promise和async / await构造之类的问题,并讨论使用事件。




编程语言中的异步


JavaScript本身是一种同步的单线程编程语言。 这意味着您无法在并行运行的代码中创建新线程。 但是,计算机本质上是异步的。 即,无论主程序执行流程如何,都可以执行某些动作。 在现代计算机中,每个程序都分配了一定数量的处理器时间,当该时间用完时,系统还会将资源分配给另一个程序一段时间。 这种切换是周期性执行的,执行速度如此之快,以至于人们根本无法注意到它,因此,我们认为我们的计算机可以同时执行许多程序。 但这是一种幻想(更不用说多处理器机器了)。

在程序的内部,使用中断-信号传输到处理器,并引起系统的注意。 我们不再赘述,最重要的是要记住,在程序暂停直到需要处理器资源之前,它的异步行为是完全正常的。 在程序无法使系统正常工作的时候,计算机可以解决其他问题。 例如,使用这种方法,当程序正在等待对对其发出的网络请求的响应时,它不会阻塞处理器,直到接收到响应为止。

通常,编程语言是异步的,其中一些使程序员能够使用内置语言工具或专用库来控制异步机制。 我们正在谈论的语言包括C,Java,C#,PHP,Go,Ruby,Swift和Python。 它们中的一些允许您使用线程以异步样式进行编程,以启动新进程。

JavaScript异步


如前所述,JavaScript是一种单线程同步语言。 用JS编写的代码行按在文本中出现的顺序依次执行。 例如,下面是一个非常普通的JS程序,它演示了此行为:

const a = 1 const b = 2 const c = a * b console.log(c) doSomething() 

但是创建了JavaScript以便在浏览器中使用。 从一开始,它的主要任务就是组织与用户活动有关的事件的处理。 例如,这些事件是诸如onClickonMouseOveronChangeonSubmit等事件。 如何在同步编程模型的框架内解决此类问题?

答案在于运行JavaScript的环境。 即,浏览器允许您有效地解决此类问题,并为程序员提供适当的API。

在Node.js的环境中,有用于执行非阻塞I / O操作的工具,例如处理文件,组织网络上的数据交换等。

回呼


如果我们谈论基于浏览器的JavaScript,则可以注意到,当用户单击按钮时不可能事先知道。 为了确保系统响应此类事件,将为其创建处理程序。

事件处理程序接受事件发生时将调用的函数。 看起来像这样:

 document.getElementById('button').addEventListener('click', () => { //    }) 

此类函数也称为回调函数或回调。

回调是一个常规函数,将其作为值传递给另一个函数。 仅在发生特定事件时才调用它。 JavaScript实现了一流功能的概念。 可以将此类函数分配给变量,然后传递给其他函数(称为高阶函数)。

客户端JavaScript开发中的一种常见方法是,将所有客户端代码包装在window对象的load事件的侦听器中,该侦听器在页面准备好工作后调用传递给它的回调:

 window.addEventListener('load', () => { //  //     }) 

回调无处不在,不仅用于处理DOM事件。 例如,我们已经在定时器中使用了它们:

 setTimeout(() => { //   2  }, 2000) 

XHR请求还使用回调。 在这种情况下,看起来就像是将一个函数分配给相应的属性。 当特定事件发生时,将调用类似的函数。 在以下示例中,此类事件是请求状态更改:

 const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) {   xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send() 

handling回调中的错误处理


让我们谈谈如何处理回调中的错误。 有一种处理此类错误的通用策略,Node.js中也使用了这种策略。 它包含以下事实:任何回调函数的第一个参数都是错误对象。 如果没有错误,则将null写入此参数。 否则,将有一个错误对象,其中包含其描述和有关它的其他信息。 看起来是这样的:

 fs.readFile('/file.json', (err, data) => { if (err !== null) {   //    console.log(err)   return } // ,   console.log(data) }) 

▍回调问题


回调在简单情况下方便使用。 但是,每个回调是代码嵌套的附加级别。 如果使用了几个嵌套的回调,这将很快导致代码结构的严重复杂化:

 window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => {   setTimeout(() => {     items.forEach(item => {       //,  -      })   }, 2000) }) }) 

在此示例中,仅显示了4个级别的代码,但实际上一个级别可能会遇到很多级别,通常称为“回调地狱”。 您可以使用其他语言结构来解决此问题。

承诺和异步/等待


从ES6标准开始,JavaScript引入了新功能,这些功能使编写异步代码变得更加容易,从而消除了对回调的需求。 我们正在谈论出现在ES6中的Promise,以及出现在ES8中的async / await构造。

▍承诺


承诺(承诺对象)是使用JavaScript中的异步软件构造的一种方法,通常可以减少回调的使用。

了解承诺


承诺通常被定义为某些值的代理对象,预计将来会出现。 承诺也称为“承诺”或“承诺结果”。 尽管此概念已经存在多年,但仅在ES2015中将诺言标准化并添加到该语言中。 在ES2017中,基于诺言的async / await设计已经出现,可以认为是它们的方便替代品。 因此,即使您不打算使用常规的Promise,对它们如何工作的理解对于有效使用异步/等待构造也很重要。

诺言如何运作


承诺被调用后,它将进入挂起状态。 这意味着,导致承诺的函数将继续执行,同时在承诺中执行一些计算,然后承诺会通知它。 如果promise执行的操作成功完成,那么promise将转移到已实现状态。 据说这种诺言已成功解决。 如果操作以错误完成,则将promise置于拒绝状态。

让我们谈一谈兑现承诺。

创建承诺


用于处理promise的API为我们提供了相应的构造函数,该构造函数由形式为new Promise()的命令调用。 这是创建诺言的方式:

 let done = true const isItDoneYet = new Promise( (resolve, reject) => {   if (done) {     const workDone = 'Here is the thing I built'     resolve(workDone)   } else {     const why = 'Still working on something else'     reject(why)   } } ) 

Promis检查全局常量done ,如果其值为true ,则成功解析它。 否则,承诺将被拒绝。 使用函数的resolvereject参数,我们可以从promise中返回值。 在这种情况下,我们返回一个字符串,但是这里可以使用一个对象。

兑现承诺


我们在上面创建了一个承诺,现在考虑使用它。 看起来像这样:

 const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet   .then((ok) => {     console.log(ok)   })   .catch((err) => {     console.error(err)   }) } checkIfItsDone() 

调用checkIfItsDone()将导致isItDoneYet isItDoneYet()的执行,并导致组织等待其解决。 如果Promise成功解析,则传递给.then()方法的回调将起作用。 如果发生错误,即承诺将被拒绝,则可以在传递给.catch()方法的函数中对其进行处理。

连锁承诺


Promise方法返回promise,使您可以将它们组合成链。 这种行为的一个很好的例子是基于浏览器的API Fetch ,它是XMLHttpRequest的抽象层。 对于Node.js,有一个相当流行的npm包,它实现了Fetch API,我们将在后面讨论。 该API可用于加载某些网络资源,并且由于可以将诺言链组合在一起,因此可以组织对下载数据的后续处理。 实际上,当您通过调用fetch()函数来调用Fetch API时,就会创建一个Promise。

考虑以下链接promise的示例:

 const fetch = require('node-fetch') const status = (response) => { if (response.status >= 200 && response.status < 300) {   return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('https://jsonplaceholder.typicode.com/todos') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) }) 

在这里,我们使用npm包node-fetchjsonplaceholder.typicode.com资源作为JSON数据源。

在此示例中, fetch()函数用于使用承诺链加载TODO列表项。 执行fetch() ,将返回具有许多属性的响应 ,我们对以下属性感兴趣:

  • status是一个数字值,代表HTTP状态代码。
  • statusText -HTTP状态代码的文本描述,如果请求成功,则由字符串OK表示。

response对象具有一个json()方法,该方法返回一个Promise,根据其解析度,将以JSON格式呈现请求主体的已处理内容。

鉴于以上所述,我们描述了这段代码中发生的事情。 链中的第一个承诺由我们宣布的status()函数表示,该函数检查响应的状态,如果它指示请求失败(即HTTP状态代码不在200到299之间),则该承诺将被拒绝。 此操作导致以下事实:未执行promise链中的其他.then()表达式,我们立即进入.catch()方法,将其与错误消息一起输出到控制台,并显示文本Request failed

如果HTTP状态代码适合我们,则调用我们声明的json()函数。 由于上一个承诺(如果成功解析)将返回一个response对象,因此我们将其用作第二个承诺的输入值。

在这种情况下,我们将返回已处理的JSON数据,因此第三个承诺将收到该数据,然后在它们之前,在控制台中显示一条消息,该消息是由于请求而可能获取必要数据的结果。

错误处理


在前面的示例中,我们在承诺链上附加了.catch()方法。 如果承诺链中的某些地方出错并发生错误,或者某个承诺被拒绝, .catch()控制权转移到最近的表达式.catch() 。 这是诺言中发生错误的情况:

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

这是一个拒绝承诺后触发.catch()的示例:

 new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) }) 

级联错误处理


如果.catch()表达式中发生错误怎么办? 要处理此错误,您可以在promise链中包含另一个.catch()表达式(然后可以根据需要将尽可能多的.catch()表达式附加到链中):

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

现在让我们看一些用于管理诺言的有用方法。

Promise.all()


如果您需要在解决多个承诺后执行某些操作,则可以使用Promise.all()命令执行此操作。 考虑一个例子:

 const f1 = fetch('https://jsonplaceholder.typicode.com/todos/1') const f2 = fetch('https://jsonplaceholder.typicode.com/todos/2') Promise.all([f1, f2]).then((res) => {   console.log('Array of results', res) }) .catch((err) => { console.error(err) }) 

在ES2015中,出现了破坏性分配的语法;使用它,您可以创建以下形式的构造:

 Promise.all([f1, f2]).then(([res1, res2]) => {   console.log('Results', res1, res2) }) 

在这里,作为示例,我们考虑了API Fetch,但是Promise.all()当然允许您使用任何promise。

Promise.race()


Promise.race()命令允许您解析传递给它的承诺之一后执行指定的操作。 包含此第一个承诺的结果的相应回调仅被调用一次。 考虑一个例子:

 const first = new Promise((resolve, reject) => {   setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => {   setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result) // second }) 

使用Promise时发生未捕获的TypeError错误


如果在处理new Promise()遇到Uncaught TypeError: undefined is not a promise new Promise()错误,请确保在创建Promise()时使用new Promise()构造,而不仅仅是Promise()

▍异步/等待设计


异步/等待构造是一种现代的异步编程方法,可以简化它。 异步功能可以表示为Promise和生成器的组合,通常,这种构造是对Promise的抽象。

异步/等待设计减少了使用Promise时必须编写的样板代码量。 当诺言出现在ES2015标准中时,它们旨在解决创建异步代码的问题。 他们完成了这项任务,但是在两年内,他们共享ES2015和ES2017标准的输出,因此很明显,它们不能被视为解决问题的最终解决方案。

可以解决的问题之一就是著名的“回调地狱”,但是解决这个问题却创造了他们自己类似性质的问题。

承诺是简单的构造,可以使用更简单的语法构建某些内容。 结果,当时间到来时,出现了异步/等待构造。 它的使用使您可以编写看起来像同步的代码,但是它是异步的,尤其是它不会阻塞主线程。

异步/等待构造如何工作


异步函数返回一个promise,如以下示例所示:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } 

当需要调用类似的函数时,必须将await关键字放在命令之前。 这将导致调用它的代码等待相应诺言的允许或拒绝。 应该注意的是,必须使用async声明使用await关键字的函数:

 const doSomething = async () => {   console.log(await doSomethingAsync()) } 

合并上面的两个代码片段并检查其行为:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } const doSomething = async () => {   console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After') 

此代码将输出以下内容:

 Before After I did something 

I did something的文本会延迟3秒进入控制台。

关于承诺和异步功能


如果您使用async声明某个函数,则这意味着即使未明确完成该函数也将返回promise。 因此,例如,以下示例为有效代码:

 const aFunction = async () => { return 'test' } aFunction().then(console.log) //    'test' 

此设计与此类似:

 const aFunction = async () => { return Promise.resolve('test') } aFunction().then(console.log) //    'test' 

异步/等待的优势


通过分析以上示例,您可以看到使用async / await的代码比使用promise链接的代码或基于回调函数的代码更简单。 当然,这里我们看了非常简单的例子。 通过使用复杂得多的代码,您可以充分体验上述好处。 例如,这里是如何使用Promise加载和解析JSON数据的方法:

 const getFirstUserData = () => { return fetch('/users.json') //      .then(response => response.json()) //  JSON   .then(users => users[0]) //      .then(user => fetch(`/users/${user.name}`)) //       .then(userResponse => userResponse.json()) //  JSON } getFirstUserData() 

这是使用async / await解决相同问题的方法:

 const getFirstUserData = async () => { const response = await fetch('/users.json') //    const users = await response.json() //  JSON const user = users[0] //    const userResponse = await fetch(`/users/${user.name}`) //     const userData = await userResponse.json() //  JSON return userData } getFirstUserData() 

使用异步函数中的序列


异步功能可以轻松地组合成类似于Promise链的设计。 但是,这种结合的结果具有更好的可读性:

 const promiseToDoSomething = () => {   return new Promise(resolve => {       setTimeout(() => resolve('I did something'), 10000)   }) } const watchOverSomeoneDoingSomething = async () => {   const something = await promiseToDoSomething()   return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {   const something = await watchOverSomeoneDoingSomething()   return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {   console.log(res) }) 

此代码将输出以下文本:

 I did something and I watched and I watched as well 

简化调试


承诺很难调试,因为使用它们您不能有效地使用调试器的常用工具(例如“逐步跳过”,逐步过渡)。 可以使用与常规同步代码相同的方法来调试使用async / await编写的代码。

Node.js中的事件生成


如果您在浏览器中使用JavaScript,那么您就会知道事件在处理用户与页面的交互中起着巨大的作用。 它是关于处理由单击和鼠标移动,键盘上的击键等引起的事件。 在Node.js中,您可以使用程序员自己创建的事件。 在这里,您可以使用events模块创建自己的事件系统。 特别是,此模块为我们提供了EventEmitter类,该类的功能可用于组织事件处理。 使用此机制之前,您需要连接它:

 const EventEmitter = require('events').EventEmitter 

使用它时,我们可以使用on()emit()方法。 emit方法用于调用事件。 on方法用于配置回调,即在调用特定事件时调用的事件处理程序。

例如,让我们创建一个start事件。 发生这种情况时,我们将向控制台输出一些内容:

 eventEmitter = new EventEmitter(); eventEmitter.on('start', () => { console.log('started') }) 

为了触发此事件,使用以下构造:

 eventEmitter.emit('start') 

执行该命令的结果是,事件处理程序被调用, started的字符串进入控制台。

您可以将参数传递给事件处理程序,将它们表示为emit()方法的附加参数:

 eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23) 

在处理程序需要传递几个参数的情况下,也会发生同样的事情:

 eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100) 

EventEmitterEventEmitter还有一些其他有用的方法:

  • once() -允许您注册只能调用一次的事件处理程序。
  • removeListener() -允许您从传递给它的事件的处理程序数组中删除传递给它的处理程序。
  • removeAllListeners() -允许您删除传递给它的事件的所有处理程序。

总结


今天,我们讨论了JavaScript中的异步编程,尤其是讨论了回调,promise和async / await构造。 在这里,我们谈到了使用events模块处理开发人员描述的events 。 我们的下一个主题将是Node.js平台的网络机制。

亲爱的读者们! 为Node.js编程时,是否使用async / await构造?

Source: https://habr.com/ru/post/zh-CN424555/


All Articles