当前位置: 首页 > 后端技术 > Node.js

Node.js异步处理的各种写法

时间:2023-04-03 20:44:23 Node.js

异步“坑”最近参与了一个Node.js后台项目的开发。作为PHP开发人员,项目本身并不难上手,但开发过程却不难。成功与失败的主要原因是思维没有转变,没有从同步思维向异步思维转换。所谓同步,就是程序(进程/线程)在处理一个任务的过程中不会插入其他任务。即使遇到IO等不占用CPU的操作,也会等到它结束后再继续处理。所谓异步,就是程序(进程/线程)在处理一个任务的过程中,会插入并处理其他任务。如果遇到IO操作,当前任务会将程序(进程/线程)的控制权释放给其他任务,等待IO操作结果返回后继续处理。简单的说,同步不释放控制,异步会。众所周知,Node.js采用的是单线程异步模型,在具体的代码编写上与PHP等同步模型有着天然的区别。在具体的项目开发过程中,各种异步操作相关的关键字层出不穷,比如:.then(),function*...yield,async...await等等。为了写一个类似同步的操作,比如:“执行完步骤A再执行步骤B,得到结果”,这么简单的需求,要经过大量的反复调试验证才能解决。原因是对这些异步操作的场景和关键字的含义没有很好的理解,异步操作提供了太多的选择。让我们结合代码示例来了解如何使用这些异步操作方案。各种异步写法任务描述:项目根目录下有Jay.txt、Angela.txt、Henry.txt三个文件。依次读取并打印这三个文件的内容。下面使用各种异步处理方法来完成这个任务。回调函数constfs=require('fs');fs.readFile('Jay.txt','utf8',function(err,data){if(err)throwerr;console.log(data);fs.readFile('Angela.txt','utf8',function(err,data){if(err)throwerr;console.log(data);fs.readFile('Henry.txt','utf8',function(err,data){if(err)throwerr;控制台。日志(数据);});});});console.log("完成");1、函数fs.readFile()用于异步读取文件总而言之,函数本身是没有返回值的。读取文件内容异步返回后,通过回调函数处理。2、函数fs.readFile()的第二个参数是可选参数。如果指定了编码方式,则返回编码方式对应的字符串;如果不指定,则返回文件的二进制内容,对应的类型为Buffer,可以通过buf.toString()方法将其转换为对应的字符串。3.回调函数的第一个参数必须是错误对象。如果没有错误,则错误对象的值为空。执行程序:$node0A_callback_01.jsfinishHello,我是Jay。你好,我是Angela。你好,我是Henry。程序首先返回finish,因为函数fs.readFile()是异步处理的。直接继续处理,返回文件内容后通过注册的回调函数处理。串行和并行通常,人们总是分不清同步、异步、串行和并行之间的区别。他们简单的认为同步就是串行,异步就是并行。这样说似乎是对的也是错的。同步和异步是从程序(进程/线程)执行的角度来看的。同步和异步的概念和区别在文章开头已经简单提到了。如果程序执行过程中没有任务切换,即:做当前任务的一件事情,等待这个任务完成,再做当前任务的下一件事,直到当前任务完成,这样方法是同步。如果在程序执行过程中发生了任务切换,即:做当前任务的一件事情,不等这个任务完成,直接转到其他任务,再做当前任务的下一件事,等等。在当前任务完成之前,此方法是异步的。串行和并行是从任务(事物)的角度来看的。如果多个任务(事物)不能同时做,而做完一个就只能做下一个,那么这些任务(事物)就称为串行的。如果多个任务(事物)可以同时完成,这些任务(事物)就称为并行。同步是串行的这句话在一定程度上是正确的,因为同步程序在完成一件事情之后,会去做接下来的事情。从两件事情的角度来看,是不会同时做的,所以同步程序只能串行的做事情。异步就是并行的说法并不是这样的。异步程序可以选择串行或并行地做事。事情是串行做还是并行做,要看具体的业务场景。对于一个任务下的两个事物A和B,如果B依赖于A的结果,则需要序列化;如果B不依赖于A的结果,则可以并行化。仍然以本文中三个文件的读取为例。上面的代码示例是串行执行的,依次读取“Jay.txt”、“Angela.txt”、“Henry.txt”的内容并打印出来。改成并行执行怎么办?简单修改一下,如下。constfs=require('fs');飞秒。readFile('Jay.txt','utf8',function(err,data){if(err)throwerr;console.log(data);});飞秒。readFile('Angela.txt','utf8',function(err,data){if(err)throwerr;console.log(data);});fs.readFile('Henry.txt','utf8',function(err,data){if(err)throwerr;console.log(data);});console.log("完成");执行程序:finish你好,我是Angela。你好,我是Jay。你好,我是亨利。从结果中也可以看出,由于三个文件是并行读取的,所以先读取哪个是随机的,与代码编写的先后顺序无关。按顺序写的代码会按顺序执行。这是典型的同步编程思想。必须尽快改变,否则迟早会“翻车”。Promise对象Promise对象可以表示异步操作的状态和结果。使用它提供的.then()方法可以将多个异步操作串联起来。.then()方法本身也返回一个Promise对象。也是顺序读取三个文件的任务。示例如下:varreadFilePromise=require('fs-readfile-promise');readFilePromise('Jay.txt','utf8').then(function(data){console.log(data);})。然后(function(){returnreadFilePromise('Angela.txt','utf8');}).then(function(data){console.log(data);}).then(function(){returnreadFilePromise('Henry.txt','utf8');}).then(function(data){console.log(data);}).catch(function(err){console.log(err);});console.log("完成");执行程序:$node0B_promise_01.jsfinishHello,I'mJay.Hello,I'mAngela.Hello,I'mHenry.Promise对象有pending(初始)、fulfilled(成功)、rejected(失败)三种状态。当异步操作成功时,Promise对象从pending状态变为fulfilled状态,并将成功结果传递给.then()方法(也称为onfulfilled函数)的第一个参数;当异步操作失败时,Promise对象从pending状态变为rejected状态,并将失败信息传递给.then()方法(也称为onrejected函数)的第二个参数。如果没有指定第二个参数,则将失败信息传递给.catch()方法的参数(也称为onrejected函数)。上面的程序可以将上一个文件的处理和下一个文件的读取合并为一个.then()。示例如下:varreadFilePromise=require('fs-readfile-promise');readFilePromise('Jay.txt','utf8').then(function(data){console.log(data);returnreadFilePromise('Angela.txt','utf8');}).then(函数(数据){console.log(数据);returnreadFilePromise('Henry.txt','utf8');}).then(函数(数据){console.log(data);}).catch(function(err){console.log(err);});console.log("finish");.then(),可以返回一个Promise对象,可以返回一个基本类型的值(数字、字符串、布尔值),也可以什么都不返回(直接return;),甚至可以省略return语句。这些场景下的处理方式可以参考:.then()方法返回值说明。生成器函数生成器函数(generatorfunction)使用function*关键字定义,函数中使用yield关键字进行流程控制。Yield后面可以跟任何表达式(普通同步表达式、Promise对象、Generator函数)。需要注意的是,yield关键字一定要放在Generator函数中,否则运行时会报错!Generator函数的返回值称为Generator对象(generatorobject)。Generator对象有一个.next()方法。.next()方法每次执行时,都会迭代到Generator函数的下一条yield语句,并返回一个对象。该对象包含两个属性:value和done,value存储yield后表达式的值;done是一个布尔值,表示Generator函数是否已经执行。同样是顺序读取三个文件的任务,例子如下:varreadFilePromise=require('fs-readfile-promise');function*generator(){yieldreadFilePromise('Jay.txt','utf8');yieldreadFilePromise('Angela.txt','utf8');yieldreadFilePromise('Henry.txt','utf8');}letgen=generator();gen.next().value.then(function(data){console.log(data);gen.next().value.then(function(data){console.log(data);gen.next().value.then(function(data){console.log(data);gen.next();//返回:{value:undefined,done:true},表示生成器函数执行完成});});});console.log("完成");执行程序:$node0C_generator_01.jsfinishHello,我是Jay。你好,我是Angela。你好,我是Henry。在这个例子中,generator()是一个生成器函数,它的返回值gen是一个生成器对象。gen.next()返回的对象结构如下。{value:Promise{},done:false}其中,gen.next().value是一个Promise,也就是说yield之后的readFilePromise()函数返回一个Promise对象。需要注意的是,生成器函数本身包含的各种异步操作,不能按顺序依次执行。如果要实现串行执行,还是需要配合Promise对象及其.then()函数,如本例所示。co函数库co函数库是做什么用的?co函数库是Generator函数的执行者。简单来说,co函数库就是用来将上一节手动执行Generator函数的过程自动化。这样,用同步思维写异步代码的想法就变成了现实。作为一个曾经是完全同步思维的程序员,他终于看到了曙光。同样是顺序读取三个文件的任务,例子如下:varco=require('co');varreadFilePromise=require('fs-readfile-promise');//generator()是一个生成器函数function*generator(){letdata=yieldreadFilePromise('Jay.txt','utf8');控制台日志(数据);data=yieldreadFilePromise('Angela.txt','utf8');控制台日志(数据);data=yieldreadFilePromise('Henry.txt','utf8');console.log(data);}letgen=generator();//gen是一个生成器对象co(generator()).then(function(){console.log('Generatorfunctionisfinished!');});console.log("完成");执行程序:$node0D_co_01.jsfinish你好,我是杰。你好,我是安吉拉。你好,我是亨利。生成器功能完成!当然,用好co的前提是有一些注意事项需要知道:1.在配合co函数使用的Generator函数中,yield之后的异步操作需要返回一个Promise对象,否则达不到想要的同步效果无法实现;2.co函数本身会返回一个Promise对象,所以如本例所示,之后可以使用.then()方法添加回调函数。asyncfunctionco库函数把Generator函数的执行简化了很多,还能再简单点吗?答案是:对,就是async函数。async函数相对于Generator函数,可以简单理解为:将Generator函数中的*改为async,将yield改为await,成为async函数。与Generator函数相比,async函数本身内置了一个executor,因此不需要像Generator函数那样引入额外的executor(如:coexecutor);async...await与function*...yield相比,语义更加清晰:async表示函数中存在异步操作,await表示需要等待异步操作的结果返回;await后面不仅可以跟Promise对象,还可以跟基本类型的值,比如:数字,字符串,布尔值,yield后面必须跟一个Promise对象;async函数的返回值也是依次读取三个文件的任务,如下:varreadFilePromise=require('fs-readfile-promise');异步函数asyncReadFile(){letdata=awaitreadFilePromise('Jay.txt','utf8');控制台日志(数据);data=awaitreadFilePromise('Angela.txt','utf8');控制台日志(数据);data=awaitreadFilePromise('Henry.txt','utf8');控制台日志(数据);return"Asyncfunctionisfinished!"}asyncReadFile().then(function(data){console.log(data);});console.log("finish");执行程序:$node0E_async_await_01.jsfinishHello,I我是Jay。你好,我是Angela。你好,我是Henry。Async函数完成了!从这个例子可以看出,除了async和await这两个关键字,整体代码的格式与函数调用和同步代码完全一样。可见,编写异步代码的最终目的是让编写异步代码像同步代码一样简单方便。不过悲催的是,对于一个刚接触JS的人来说,要慢慢搞清楚这些需要很长时间,而且学习成本不低。