当前位置: 首页 > Web前端 > HTML5

走一步再走一步,揭开co的神秘面纱

时间:2023-04-05 19:08:10 HTML5

一步一个脚印,揭开co自动执行函数库的神秘面纱,大名鼎鼎的koa也用它来管理异步流程控制,同步异步任务的写入,爽飞,摆脱长常设回调地狱问题。如何使用首先我们根据co的官方文档稍微改动一下看看co的使用方法,然后一步步分析源码(本文分析的co版本为4.6.0)。yield后面经常看到的可以跟踪的类型promisesarray(parallelexecution)objects(parallelexecution)generatorfunctions(delegation)promisesletco=require('co')letgenTimeoutFun=(delay)=>{return()=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(`delayTime:${delay}`)},delay)})}}lettimeout1=genTimeoutFun(1000)lettimeout2=genTimeoutFun(200)co(function*(){leta=yieldtimeout1()console.log(a)//delayTime:1000letb=yieldtimeout2()console.log(b)//delayTime:200return'end'}).then((res)=>{console.log(res)})arrayletco=require('co')letgenTimeoutFun=(delay)=>{return()=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(`delayTime:${delay}`)},delay)})}}lettimeout1=genTimeoutFun(1000)lettimeout2=genTimeoutFun(200)co(function*(){让一个=yield[timeout1(),timeout2()]console.log(a)//['delayTime:1000','delayTime:200']return'end'}).then((res)=>{console.log(res)//end})objectsletco=require('co')letgenTimeoutFun=(delay)=>{return()=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(`delayTime:${delay}`)},delay)})}}lettimeout1=genTimeoutFun(1000)lettimeout2=genTimeoutFun(200)co(function*(){leta=yield{timeout1:timeout1(),timeout2:timeout2()}console.log(a)//{timeout1:'delayTime:1000',timeout2:'delayTime:200'}return'end'}).then((res)=>{console.log(res)//end})generatorfunctionsletco=require('co')letgenTimeoutFun=(delay)=>{return()=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(`delayTime:${delay}`)},delay)})}}lettimeout1=genTimeoutFun(1000)lettimeout2=genTimeoutFun(200)function*gen(){leta=yieldtimeout1()console.log(a)//delayTime:1000letb=yieldtimeout2()console.log(b)//delayTime:200}co(function*(){yieldgen()return'end'}).then((res)=>{console.log(res)//end})最后,让co=require('co')co(function*(name){console.log(name)//qianlongo},'qianlongo')从co函数的第二个参数开始,也就是传入的generator函数可以接收到的实际参数,开始分析源码。大家可以将上面的代码复制到本地测试看看效果。接下来我们一步步分析co的源码。首先,通过上面的例子,我们可以发现co函数本身接收了一个generator函数,co执行后返回Promise函数co(gen){varctx=this;varargs=slice.call(arguments,1)//我们将所有内容包装在一个promise中以避免promise链,//这会导致内存泄漏错误。//参见https://github.com/tj/co/issues/180returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);//xxx});}在Promise内部,首先执行从外部传入的gen。如果执行结果没有next属性(如果是函数),则直接返回,执行成功回调resolve(gen),否则获取指针对象接下来继续看...onFulfilled();/***@param{Mixed}res*@return{Promise}*@apiprivate*/functiononFulfilled(res){varret;试试{ret=gen.next(res);//上面gen执行完后,用生成器generator将指针指向下一个位置}catch(e){returnreject(e);}下一个(退);//然后执行next,就是这样实现的反复调用自己,自动流程控制,注意ret(即上次执行gen.next后返回的对象{value:xxx,done:trueorfalse})}/***@param{Error}err*@return{Promise}*@apiprivate*/functiononRejected(err){varret;尝试{ret=gen.throw(err);}catch(e){返回拒绝(e);}next(ret);}我想你可以把onFulfilled和onRejected看作是返回的Promise的resolve和reject。而onFulfilled也包裹了原来generatorgenerator的next方法,大概是为了捕获错误(你见过内部的trycatch吗?)嗯,我们看到co内部将指针移动到第一个位置后,然后执行内部的next方法,然后关注这个函数functionnext(ret){//如果整个generator函数的内部状态已经执行完毕,将Promise的状态设置为success,执行resolveif(ret.done)returnresolve(ret.value);//这一步是将ret的值转换为Promise形式varvalue=toPromise.call(ctx,ret.value);//这里的关键是co实现了自己的调用Self,实现流程自动化的关键//注意这里使用了value.then,即在返回值上加上成功和失败的回调,在成功的回调中执行onFulfilled,然后调用内部的next函数//那不就保证流程完全按照你写的顺序进来了吗?if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);//抛出错误,yield后面只能跟以下类型returnonRejected(newTypeError('Youmayonlyyieldafunction,promise,generator,array,orobject,'+'butthefollowingobjectwaspassed:"'+String(ret.value)+'"'));}聪明的你,你是不是已经明白co是如何将异步过程自动管理的了,但是我对下一个函数中的toPromise函数还有疑问。它到底是做什么的?这使得co(generatorFun)中的yield能够支持数组、对象、生成器函数和其他形式。stepbystepfunctiontoPromise(obj){//obj不存在,直接返回if(!obj)returnobj;//如果obj已经是一个Promise,直接返回if(isPromise(obj))returnobj;//如果是generator函数或者generator生成器,就像自己调用co函数一样,手动传给co执行if(isGeneratorFunction(obj)||isGenerator(obj))returnco.call(this,obj);//如果obj既不是Promise,也不是isGeneratorFunction和isGenerator,如果是普通函数(需要符合thunk函数规范),将函数包装成Promise形式if('function'==typeofobj)returnthunkToPromise.调用(这个,对象);//如果是数组形式,则将其包裹在arrayToPromise中if(Array.isArray(obj))returnarrayToPromise.call(this,obj);如果(isObject(obj))返回objectToPromise.call(this,obj);returnobj;}首先,如果obj不存在,则直接返回。你想,co本来依赖最后一个指针返回的值是Promise还是其他。如果它返回{value:false,done:false}则不需要将false值转换为Promise形式。接下来如果obj本身是Promise,则直接返回,内部isPromise函数进行判断。让我们看看它是如何实现的。functionisPromise(obj){return'function'==typeofobj.then;}其实就是判断obj的then属性是否为函数,然后继续。如果是generatorfunction或者generator生成器,就像自己调用co函数一样,手动传给co去执行。isGeneratorFunctionfunctionisGeneratorFunction(obj){varconstructor=obj.constructor;如果(!构造函数)返回假;if('GeneratorFunction'===constructor.name||'GeneratorFunction'===constructor.displayName)returntrue;returnisGenerator(constructor.prototype);}通过obj的constructor属性判断是否属于GeneratorFunction。最后,如果constructor属性不确定,则使用isGenerator判断obj的原型是否为generator生成器。functionisGenerator(obj){return'function'==typeofobj.next&&'function'==typeofobj.throw;}判断条件比较简单,需要满足两个条件,一个是obj.nextifafunction,一个isobj.throwifafunctionnext继续看obj是否既不是Promise,也不是isGeneratorFunction和isGenerator,如果是普通函数,将函数包装成Promise形式,这里主要需要看thunkToPromisefunctionthunkToPromise(fn){varctx=这个;//thunk函数被包装成一个PromisereturnnewPromise(function(resolve,reject){//执行这个thunkfunctionfn.call(ctx,function(err,res){//注意传入的第一个值thunk函数内部接收到的回调函数第一个参数是err,如果出现err,当然需要rejectif(err)returnreject(err);//当参数超过两个时,参数合并为一个数设置if(arguments.length>2)res=slice.call(arguments,1);//最后执行成功的回调resolve(res);});});}接下来就是重头戏了,如果在co处理yield之后的一个数组呢?主要是arrayToPromise函数的作用functionarrayToPromise(obj){//Promise.all用于将obj中的多个promise实例(当然你也可以在数组中填入thunk函数,generator函数等)重新打包成一个最后返回一个新的PromisereturnPromise.all(obj.map(toPromise,this));}还有最后一个判断,如果obj是一个对象呢?functionobjectToPromise(obj){//构造一个与传入对象相同构造函数的对象,结果也是varresults=newobj.constructor();//获取对象的键varkeys=Object.keys(obj);//将Promise的属性存储在obj中varpromises=[];for(vari=0;i