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

协源码分析及其实践

时间:2023-04-03 18:36:59 Node.js

本文源自个人博客,如需转载请注明出处。为了更好的阅读体验,可以直接进入我的个人博客。前言知识储备阅读本文需要对Generator和Promise有一个基本的了解。这里我简单介绍一下两者的用法。Generator关于Generator的用法,推荐在MDN上解释function*函数,很详细。一句话,生成器函数是回调地狱的一种解决方案。它类似于promise,但是可以同步的方式编写代码,避免了promise的链式调用。它的执行过程是调用生成器函数(generatorfunction)后,会返回一个iterator(迭代)对象,即Generator对象,但不会立即执行里面的代码。它有几个方法,next()、throw()和return()。调用next()方法后,它会找到第一个yield关键字(直到找到程序的底部或return语句)。程序每次运行到yield关键字时,程序都会暂停,保存当前环境变量的值。然后就可以跳出当前运行环境执行yield后面的代码,然后返回结果。返回的结果是一个对象,类似于{value:'',done:false},value表示本次yield执行后返回的结果。如果它是一个Promise实例,它返回解析后的值。done指示迭代器是否已完成执行。如果为真,则说明当前生成器函数已经产生了上次的输出值,即生成器函数返回了。这是一个简单的例子:constgen=function*(){letindex=0;while(index<3)yieldindex++;返回“全部完成。”};常量g=gen();console.log(g。构造函数);//输出:GeneratorFunction{}console.log(g.next());//输出:{值:0,完成:假}console.log(g.next());//输出:{值:1,完成:false}console.log(g.next());//输出:{值:2,完成:false}console.log(g.next());//output:{value:'Alldone.',done:true}console.log(g.next());//output:{value:undefined,done:true}Promise关于Promise的用法,可以参考我之前写的一篇文章《关于ES6中Promise的用法》,写的比较详细。Promise对象用来表示一个异步操作的最终完成(或失败)及其结果值(简单来说就是处理异步请求)。Promise的核心在于内部状态的转换,无论是rejected、resolved还是pending,以及原型链上的then()方法,可以传递这个状态转换后的返回值。进入正题由于实际需要,这几天学习了koa2.x框架,但是不再推荐使用生成器函数。建议使用async/await组合。koa2.x的最新用法:async/await(nodev7.6+):constkoa=require('koa');constapp=newKoa();app.use(async(ctx,next)=>{conststart=Date.now();awaitnext();constms=Date.now()-开始;console.log(`${ctx.method}${ctx.url}-${ms}ms`);});常见用法:constKoa=require('koa');constapp=newKoa();//responseapp.use(ctx=>{ctx.body='HelloKoa';});app.listen(3000);由于本地Node版本是v6.11.5,使用async/await需要Node版本v7.6以上,请问有没有什么模块可以让koa2.x版本的语法兼容koa1.x的语法。koa1.x语法的关键是生成器/yield组合。通过yield,可以很方便的暂停程序的执行,改变执行环境。这时找到了TJ写的co模块,可以同步异步过程,还有koa-convert模块等,这里重点介绍co模块。co在koa2.x中的用法如下:constKoa=require('koa');constapp=newKoa();constco=require('co');//responseapp.use(co.wrap(function*(ctx,next){yieldnext();//yieldsomeAyncOperation;//...ctx.body='co';}));app.listen(3000);co模块不仅可以作为中间件配合koa框架使用转换函数,还支持生成器函数的批量执行,这样就不需要手动多次调用next()获取结果。它支持的参数是函数、承诺、生成器、数组和对象。//co的源代码returnonRejected(newTypeError('你只能产生一个函数、promise、生成器、数组或对象,'+'但是传递了以下对象:"'+String(ret.value)+'"'));下面是co传递生成器函数的示例://这里模拟生成器函数调用constco=require('co');co(gen).then(data=>{//output:then:ALLDone.console.log('then:'+data);});function*gen(){letdata1=yieldpro1();//输出:pro1已解决,data1=Iampromise1console.log('pro1已解决,data1='+data1);让data2=yieldpro2();//输出:pro2已解决,data2=Iampromise2console.log('pro2已解决,data2='+data2);return'ALLDone.'}functionpro1(){returnnewPromise((resolve,reject)=>{setTimeout(resolve,2000,'Iampromise1');});}functionpro2(){returnnewPromise((resolve,reject)=>{setTimeout(resolve,1000,'我是promise2');});}我觉得co()这个函数很神奇,它经历了怎样的转换?带着好奇,我看了co的源码。协源码分析主要脉络。调用co函数后,返回一个Promise实例。co的思想是将传入的一个参数合法化,然后通过转换成Promise实例的方式返回。如果参数fn是生成器函数,也可以自动遍历,执行生成器函数中yield关键字后面的内容,并返回结果,即不断调用fn().next()方法,然后将返回的Promise实例的resolved值传入,从而达到同步执行生成器函数的效果。这里需要注意的是,co函数中最重要的是理解Promise实例和Generator对象,这是co函数中程序自动遍历和执行的关键。先解释一下co模块中最重要的两个部分,一个是generator函数的自动调用,一个是参数的Promise。一、生成器函数的自动调用(中文部分是我的解释):functionco(gen){//保存当前执行环境varctx=this;//截取调用函数时传递的参数varargs=slice.call(arguments,1)//我们将所有内容包装在一个promise中以避免promise链,//这会导致内存泄漏错误。//参见https://github.com/tj/co/issues/180//返回一个Promise实例returnnewPromise(function(resolve,reject){//如果gen是一个函数,返回一个新的gen的副本function,//绑定this的指针,即ctxif(typeofgen==='function')gen=gen.apply(ctx,args);//如果gen不存在或者gen.next不是一个函数//表示gen已经被调用,//那么就可以直接resolve(gen)并返回Promiseif(!gen||typeofgen.next!=='function')returnresolve(gen);//调用gen.next()函数第一次调用,如果存在,onFulfilled();/***@param{Mixed}res*@return{Promise}*@apiprivate*/functiononFulfilled(res){varret;try{//尝试获取下一次代码执行后的返回值yieldret=gen.next(res);}catch(e){returnreject(e);}//处理结果next(ret);}/***@param{Error}err*@return{Promise}*@apiprivate*/functiononRejected(err){varret;try{//尝试抛出错误ret=gen.throw(err);}catch(e){返回拒绝(e);}//处理结果next(ret);}/***获取生成器中的下一个值,*返回一个承诺。**@param{Object}ret*@return{Promise}*@apiprivate*///这个next()函数是最关键的部分,//它几乎包含了generator自动调用next(ret)实现的核心函数{//如果ret.done===true,//证明生成器函数已经执行//返回值if(ret.done)returnresolve(ret.value);//将ret.value转换为Promise对象并继续调用varvalue=toPromise.call(ctx,ret.value);//如果存在,将控制权交给onFulfilled和onRejected,//实现递归调用if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);//否则直接抛出错误returnonRejected(newTypeError('Youmayonlyyieldafunc运动、承诺、生成器、数组或对象,'+'但传递了以下对象:“'+String(ret.value)+'”'));}});}对于上面代码中的onFulfilled和onRejected,我们可以把它们看作是co模块对resolve和reject的封装的增强版。第二,参数是Promise。我们看一下co中toPromise的实现:functiontoPromise(obj){if(!obj)returnobj;如果(isPromise(obj))返回obj;如果(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);if('function'==typeofobj)returnthunkToPromise.call(this,obj);如果(Array.isArray(obj))返回arrayToPromise.call(this,obj);如果(isObject(obj))返回objectToPromise.call(this,obj);returnobj;}toPromise的本质是通过判断参数的类型,然后将控制权转移到不同的参数处理函数,以获得预期的返回值。关于参数类型的判断,看源码就可以理解,比较简单。下面重点分析一下objectToPromise的实现://获取对象的所有可遍历键varkeys=Object.keys(obj);var承诺=[];for(vari=0;i{//继续循环returnnext(data);//promiseresolve});}}};这样我们就可以在test.js文件中调用它://test.jsconstmyCo=require('./my-co');constfs=require('fs');letgen=function*(){让data1=yieldpro1();console.log('data1:'+data1);让data2=yieldpro2();console.log('data2:'+data2);让data3=yieldpro3();安慰。日志('数据3:'+数据3);让data4=yieldpro4(data1+'\n'+data2+'\n'+data3);console.log('data4:'+data4);返回'全部完成。'};//调用myComyCo(gen);//延迟两秒resolvefunctionpro1(){returnnewPromise((resolve,reject)=>{setTimeout(resolve,2000,'promise1已解决');});}//延迟一秒resolvefunctionpro2(){returnnewPromise((resolve,reject)=>{setTimeout(resolve,1000,'promise2resolved');});}//将HelloWorld写入./1.txt文件functionpro3(){returnnewPromise((resolve,reject)=>{fs.appendFile('./1.txt','HelloWorld\n',function(err){resolve('write-1成功');});});}//将内容写入./1.txt文件functionpro4(content){returnnewPromise((resolve,reject)=>{fs.appendFile('./1.txt',content,function(err){resolve('write-2success');});});}控制台输出结果://outputdata1:promise1resolveddata2:promise2resolveddata3:write-1successdata4:write-2success./1.txt文件内容:HelloWorldpromise1resolvedpromise2resolvedwrite-1success从上面我们可以看出运行结果符合我们的预期。虽然这个runner很简单,只支持Promise实例,不支持多参数,但是它引导我们去思考如何展示我们的代码,有效的避免了多个then,用同步的方式写异步代码。Promise解决的是回调地狱(callbackhell)的问题,而Generator解决的是写代码的方式。哪个更好或哪个更差完全是个人喜好问题。总结上面对co部分源码本质的分析,我们讲了co函数中generator函数的自动遍历和执行机制,也讲了co中最关键的objectToPromise()方法。文章后面我们写了一个自己的生成器函数遍历器,主要是next()方法,可以检测我们yield后的Promise操作是否完成。如果生成器done的状态还没有设置为true,那么继续调用next(val)方法,将上次yield操作得到的值传递过去。有时候在引用别人的模块出现问题的时候,如果在网上找不到自己期望的答案,那么我们可以根据自己的能力有选择地分析作者的源码。看源码是一种很好的成长方式。坦白说,这是我第一次深入分析模块的源码。co模块的源代码只有大约230行,包括注释和空行,因此这是一个很好的切入点。代码虽然少,但并不容易看懂。以上如有问题,欢迎反馈。感谢您的支持。参考链接MDN-Promise详解MDN-GeneratorobjectTJ-co的源码及其用法