koa被认为是第二代web后端开发框架。与上一代express相比,它最大的特点无疑是解决了回调金字塔的问题,让异步的写法更加简洁。在使用koa的过程中,一直对koa的内部实现机制很好奇。最近终于有时间深入研究koa的一些原理了。在这里我会写一个系列的文章来记录下我的学习心得和感悟。在我看来,koa的核心功能就是famousco。koa正是基于这个功能实现了异步回调同步和中间件流程控制。当然,本文不分析协源码。打算在整个系列文章中逐步讲解如何实现koa中间件的流程控制原理,以及koa的异步回调同步写法的实现原理。最后,在理解了这些之后,在此基础上,实现一个简单的合赞功能。首先,本文只讲koa的中间件流程控制的原理。一、koa中间件执行过程关于koa中间件的执行过程,官网上有一个很经典的例子,有兴趣的可以去看看,这里我想修改的更简单一点:varkoa=require('koa');varapp=koa();app.use(function*(next){console.log('beginmiddleware1');yieldnext;console.log('endmiddleware1');});app.use(function*(next){console.log('beginmiddleware2');yieldnext;console.log('endmiddleware2');});app.use(function*(){console.log('中间件3');});app.listen(3000);运行这个例子,然后使用curl工具运行:curlhttp://localhost:3000可以看到运行后会输出:beginmiddleware1beginmiddleware2middleware3endmiddleware2endmiddleware1这个例子非常形象的代表了中间件的执行koa的机制,可以用下图中的洋葱模型来描述:通过这个执行过程,开发者可以很方便的开发一些中间件,并且很容易集成到实际的业务流程中。那么,这样的过程是如何实现和控制的呢?2、koa中的generator和compose简单来说,洋葱模型的执行过程是通过es6中的generator来实现的。不熟悉generator的同学可以看看它的特性。其中之一就是生成器函数可以像断点一样从函数的某个地方跳出,然后回来继续执行。下面的例子可以说明这个特性:vargen=function*(){console.log('begin!');//yield语句,从这里跳出来,把控制权交给anotherfunc函数。产生另一个功能;//下次回来从这里开始执行console.log('end!');}varanotherfunc(){console.log('thisisanotherfunction!');}varg=gen();varanother=g.next();//'begin!'//another是一个对象,其中value成员是返回的anotherfunc函数another.value();//'这是另一个函数!'g.next();//'结尾!';从这个简单的例子中,我们可以看出洋葱模型最基本的原型,即先执行后执行yield前后的语句,居中执行yield中间的代码。现在想象一下,如果yield后面的函数本身就是一个生成器会怎样?事实上,它是上述示例的扩展:vargen1=function*(){console.log('begin!');产量g2;console.log('end!');}vargen2=function*(){console.log('begin2');产生另一个功能;console.log('end2');}varanotherfunc(){console.log('这是另一个函数!');}varg=gen();varg2=gen2();varanother1=g.next();//'开始!';varanother2=another1.value.next();//'开始2';another2.value();//'这是另一个函数!';another1.value.next();//'结束2';g.next();//'结尾!';可以看出,基本上就是用上面的例子,再加一个嵌套而已,原理是一样的。在koa中,每个中间件生成器都有一个next参数。在我们的例子中,g2可以看作是g函数的下一个参数。事实上,koa就是这样做的。当使用app.use()挂载所有中间件时,koa有一个koa-compose模块用于串联所有生成器中间件,基本上是将后面的生成器赋值给前面生成器的下一个参数。koa-compose的源代码非常简单和简短。这是我自己实现的一个:functioncompose(middlewares){returnfunction(next){vari=middlewares.length;varnext=function*(){}();while(i--){next=middlewares[i].call(this,next);}下一个返回;}}使用我们自己的compose修改上面的例子,是的,它更接近于koa的形式:functioncompose(middlewares){returnfunction(next){长度;varnext=function*(){}();while(i--){next=middlewares[i].调用(这个,下一个);}下一个返回;}}vargen1=function*(next){console.log('开始!');接下来屈服;console.log('end!');}vargen2=function*(next){console.log('begin2');接下来屈服;console.log('end2');}vargen3=function*(next){console.log('这是另一个函数!');}varbundle=compose([gen1,gen2,gen3]);varg=bundle();varanother1=g.next();//'开始!';varanother2=another1.value.next();//'开始2';另一个2。值.下一个();//'这是另一个函数!';another1.value.next();//'结束2';g.next();//'zhd!';如何?有没有一种koa中间件写的感觉?但是目前,我们还是一步步手动执行我们的洋葱模型。我们可以编写一个函数来自动执行我们的模型吗?3.让洋葱模型自动运行:写一个run函数在上面的例子中,我们可以在最后的代码中看到一个规则,基本上外层生成器调用next方法将控制权转移到内层,内层继续调用Next,方法交给内层处理。整个过程可以写在一个嵌套的函数中。话不多说,直接上代码:functionrun(gen){varg;if(typeofgen.next==='function'){g=gen;}else{g=gen();}functionnext(){vartmp=g.next();//如果tmp.done为真,则证明生成器执行完成,返回。如果(tmp.done){返回;}elseif(typeofg.next==='function'){run(tmp.value);下一个();}}next();}functioncompose(middlewares){returnfunction(next){vari=middlewares.长度;varnext=function*(){}();while(i--){next=middlewares[i].调用(这个,下一个);}下一个返回;}}vargen1=function*(next){console.log('开始!');接下来屈服;console.log('end!');}vargen2=function*(next){console.log('begin2');接下来屈服;console.log('end2');}vargen3=function*(next){console.log('这是另一个函数!');}varbundle=compose([gen1,gen2,gen3]);运行(捆绑);run函数接受一个generator,它的内部执行其实就是我们之前例子的一个简化,使用递归的方式来执行。运行这个例子,你可以看到结果和我们之前的例子是一样的。至此,我们已经基本说明了koa中的中间件onion模型是如何自动执行的。其实koa中使用的co函数的一部分功能就是实现我们这里写的run函数的功能。值得注意的是,本文只着重分析中间件执行过程的实现,暂时不考虑异步回调同步的原理。下一篇文章,我将带大家继续探索koa中异步回调的同步写入机制。本文的代码可以在github上找到:https://github.com/mly-zju/async-js-demo,其中process_control.js文件就是本文的源代码。另外,欢迎多多关注我的个人博客^_^我会不定时更新我的??技术文章~
