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

异步开发流程——Generators+co让异步更优雅

时间:2023-04-03 20:06:09 Node.js

阅读原文Generators介绍Generator功能是ES6提供的一种异步编程解决方案。它是用于生成遍历器函数的生成器。功能完全不同。IteratortraverserJavaScript原有的表示“集合”的数据结构,主要有Array和Object,ES6中加入了Set和Map,这样就有四种数据集合,也可以组合使用,比如数组的成员是Map还是Object,所以需要一个统一的接口机制来处理所有不同的数据结构。迭代器就是这样一种机制。它是一种接口,为不同的数据结构提供统一、方便的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作,即顺序处理数据结构。的所有成员。Iterator遍历器实际上是一个指针对象,上面有一个next方法。第一次next调用指向数据结构的第一个成员,第二次next调用指向第二个成员,直到指针指向最后一个成员。我们可以使用ES6扩展运算符...和??for...of...来遍历具有Iterator接口的数据结构。需要注意的是,Object本身没有Iterator接口,所以我们不能通过...将一个对象扩展成数组,会报错。我们可以通过代码手动实现Object类型的Iterator接口。//将Iterator接口扩展到对象//通过Generator函数将Iterator接口扩展到ObjectObject.prototype[Symbol.iterator]=function*(){for(varkeyinthis){yieldthis[key];}};//测试迭代器接口letobj={a:1,b:2,c:3};让arr=[...obj];控制台日志(arr);//上面的[1,2,3]我们其实是通过ES6的Generator函数,简单粗暴的为Object类型实现了Iterator接口。后面我们会简单模拟一下Generator生成器。AnalogGeneratorGenerator函数就是一个生成器,调用后会返回给我们一个Iterator遍历器对象。对象中有一个next方法。调用next一次帮我遍历一次。返回值是一个内部有值和完成的对象。属性,value属性表示当前遍历的值,done属性表示遍历是否完成。如果遍历完成,继续下一次调用,则返回对象的value属性值为undefined,done属性值为true。就像为我们提供了一个暂停功能,每次都需要我们手动调用next来进行下一次遍历。我们利用ES5根据Generator的特点简单模拟一个遍历器生成函数://模拟遍历器生成函数functioniterator(arr){vari=0;return{next:function(){vardone=i>=arr.length;var值=!完成?arr[i++]:未定义;返回{值:值,完成:完成};}};}测试模拟迭代器生成函数://测试迭代器函数vararr=[1,3,5];//遍历器varresult=iterator(arr);result.next();//{value:1,done:false}result.next();//{value:3,done:false}result.next();//{value:5,done:false}result.next();//{value:undefined,done:true}Generator的基本用法是在普通函数中的function关键字后加一个*代表声明一个生成器函数,执行后返回一个遍历器对象。每次调用遍历器的next方法,遇到yield关键字就暂停执行,将yield关键字后的值作为返回对象中的值,如果函数有返回值,返回值将作为调用next方法完成遍历后返回的对象中value的值。如果已经遍历完成,再次调用next时,这个value的值就会变成undefined。//生成器函数function*gen(){yield1;产量2;return3;}//遍历器letit=gen();it.next();//{value:1,done:false}it.next();//{value:2,done:false}it.next();//{value:3,done:true}it.next();//{value:undefined,done:true}在Generator函数中,可以使用变量接收yield关键字执行后的返回值,但接收到的值不是yield关键字后表达式执行的结果,而是遍历器下次调用next方法时传入的参数。也就是说我们第一次调用next方法遍历的时候不需要传递参数,因为上面没有变量接收,即使传递了参数也会被忽略。下面我们通过一个例子来体验一下这种特殊的执行机制://generatorfunctionfunction*gen(arr){leta=yield1;让b=产生a;让c=产生b;returnc;}//遍历器letit=gen();it.next();//{value:1,done:false}it.next(2);//{value:2,done:false}it.next(3);//{value:3,done:false}it.next(4);//{value:4,done:true}it.next(5);//{value:undefined,done:true}如果遍历已经完成,将上次遍历得到的值作为返回值传递给返回对象的value属性,调用next时传入的参数againlater也会被忽略,返回对象的值是undefined。在Generator函数中,如果在其他函数或方法调用的回调内部(函数的执行上下文/上下文发生变化),则不能直接使用yield关键字。//在循环中使用yield//错误的写法function*gen(arr){arr.forEach(*item=>{yield*item;});}//正确的写法function*gen(arr){for(leti=0;iGenerators结合PromisePromise也是ES6的规范,同样是解决异步的一种手段。如果你对Promise还不了解,可以看下面两篇文章:异步开发流程——Promise基本使用异步开发流程——手写一个符合Promise/A+规范的Promise,因为Generator函数在执行时遇到yield关键字,会暂停执行,然后yfieldbehind的代码可以是异步操作代码,比如Promise。如果需要继续执行,则手动调用返回遍历器的next方法。因为中间有一个等待过程,所以在执行异步代码时避免了回调函数的嵌套。写起来更同步,也更容易理解。我们来设计一个使用场景,结合Generator功能和Promise异步操作。假设我们需要使用NodeJS的fs模块读取一个文件a.txt的内容,而a.txt的内容是另一个需要读取文件b.txt的A文件名,读取b.txt最后打印阅读内容“Helloworld”。回调函数的实现://持续读取文件-异步回调//引入依赖constfs=require("fs");fs.readFile("a.txt","utf8",(err,data)=>{if(!err){fs.readFile(data,"utf8",(err,data)=>{if(!err){console.log(data);//Helloworld}});}});上面的代码并没有那么复杂,因为只有两层回调函数嵌套,但是如果回调函数嵌套太多,代码就没有那么优雅了。接下来我们将使用Generator结合Promise来实现。为了方便异步的fs方法变成Promise,我们导入util模块,转换readFile方法。//持续读取文件-Generator+Promise//引入依赖constfs=require("fs");constutil=require("util");//将readFile方法转化为Promiseconstread=util.promisify(fs.readFile);//生成器函数function*gen(){letaData=yieldread("1.txt","utf8");letbData=yieldread(aData,"utf8");returnbData;}//遍历器letit=gen();it.next().value.then(data=>{it.next(data).then(data=>{console.log(data);//你好世界});});我们只看Generator函数gen的内部执行。虽然是异步操作,但是写起来和同步差不多,也更容易理解。唯一美中不足的是需要我们自己手动调用遍历器的next和next。那么Promise实例的co库就可以帮助我们解决这个问题。co库的使用co库的作者是大名鼎鼎的NodeJS高手TJ。它基于Generator和Promise。这个库可以帮我们自动调用Generator函数返回遍历器的next方法,执行yield后面的Promise实例的then方法,所以yield之后的每一个异步操作都必须返回一个Promise实例。代码看起来是同步的,但执行实际上是异步的。我们不需要手动执行下一次遍历,这正是我们想要的。由于co是第三方模块,所以我们在使用的时候需要提前下载:npminstallco我们使用co来实现前面异步连续读取文件的案例://连续读取文件-Generator+co//引入依赖constfs=require("fs");constutil=require("util");constco=require("co");//将readFile方法转换为Promiseconstread=util.promisify(fs.readFile);//生成器函数function*gen(){letaData=yieldread("1.txt",“utf8”);letbData=yieldread(aData,"utf8");returnbData;}//使用co库而不是手动Callnextco(gen()).then(data=>{console.log(data);//Helloworld});从上面代码可以看出,co库的co函数参数是一个遍历器,即Generator函数执行完返回结果后,在co内部操作遍历器,遍历完成后返回一个Promise实例.遍历器最终返回结果的值作为then方法回调的参数,所以我们可以使用then对结果进行后续处理。co库的实现原理我们在上面使用co的过程中其实对co函数内部做了什么有所了解,主要是帮助我们在代码执行完之后调用遍历器的next和Promise实例的then调用yield,并在整个遍历完成后,返回一个新的Promise实例。接下来,我们根据上面分析的co函数原理,模拟一个简单版本的co库://文件:myCo.js——co原理//co函数,就是遍历器对象函数co(it){//returnPromiseExamplereturnnewPromise((resolve,reject)=>{//异步递归函数next(data){//第一次调用next不需要传参let{value,done}=it.next(data);if(!done){//如果遍历没有完成,调用返回Promise值的then方法.then(data=>{//如果Promise成功,继续递归,如果失败,直接执行rejectnext(data);},reject);}else{//如果遍历完成,直接执行resolve,传入值resolve(value);}}next();});}//exportmodulemodule.exports=co;验证myCo.js实现的co函数://VerifymyCo//引入依赖constfs=require("fs");constutil=require("util");constmyCo=require("./myCo");//将readFile方法转换为Promiseconstread=util.promisify(fs.readFile);//生成器函数function*gen(){letaData=yieldread("1.txt","utf8");让bData=产量d读取(aData,“utf8”);returnbData;}//使用co库而不是手动调用nextmyCo(gen()).then(data=>{console.log(data);//Helloworld});我们用自己实现的简单版本的myCo模块替换导入的co库。上面读取文件的情况依然有效,说明我们模拟的co库的核心逻辑是没问题的。与原始版本不同,它没有处理很多细节。并定义指针。如果对co库感兴趣,建议看一下TJ大神的源码。整个库非常简洁,值得学习。总结Generators相当于把一个函数拆分成几个部分执行。执行一次时,指针指向下一段要执行的代码,直到结束位置,Generators配合co库的使用在NodeJS中居多,Koa1.x版本居多。现在已经升级到Koa2.x版本,使用更多基于Generators和co库衍生的ES7新标准async/await,在异步开发过程系列的下一篇文章中会详细介绍。