摘要:需要说明的是,每次执行完函数之后,都会返回一个对象这个返回值有两个属性和,对象通过这个返回值来告诉外界函数的执行情况。函数的返回值变成这样可以发现的值变为了,因为函数已经执行完了。在规范中,新增了两个协议可迭代协议和迭代器协议。
Koa是最近比较火的一款基于Node的web开发框架。说他是一个框架,其实他更像是一个函数库,通过某种思想(或者说某种约定),将众多的中间件联系在一起,从而提供你所需要的web服务。
Koa做了两件很重要的事:
封装node的request和response对象到Context上,还提供了一些开发web应用以及api常用的方法
提供了一套流程控制方式,将众多中间件级联在一起
而我现在想讨论的就是Koa的这套流程控制的思想。
先看一段从官方文档上搬下来的代码:
var koa = require("koa"); var app = koa(); // x-response-time app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; this.set("X-Response-Time", ms + "ms"); }); // logger app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log("%s %s - %s", this.method, this.url, ms); }); // response app.use(function *(){ this.body = "Hello World"; }); app.listen(3000);
app是Koa的一个实例,通过调用app.use,向Koa内部维护的一个middlewares数组中,添加中间件。而我们所说的中间件,其实就是那个作为app.use参数的,使用奇怪方式声明的function。
在Koa中,我们约定所有的中间件都是以这种方式声明的,如果你了解ES6,那你一定见过这种声明方式。没错,这就是ES6中的generator function。Koa中,真正的中间件其实就是一个generator对象。
什么是Generator?Generator是ES6新引进的一个概念,使用Generator可以将函数的控制权交给函数外部。也就是说,你可以控制函数的执行进程。
举个例子:
function *sayHello(){ console.log("before say"); yield console.log("hello!"); console.log("end say"); } var a = sayHello(); a.next(); // 输出before say 输出hello! a.next(); // 输出end say
首先我们定义了一个叫做sayHello的generator function,它跟普通的function不同,执行sayHello(),并不会执行函数体内部的程序,但是会返回一个generator对象。因此a的值实际上长这样:
sayHello {[[GeneratorStatus]]: "suspended"}
对generator function来说,执行函数只是生成了一个generator对象,不会执行函数的内在逻辑,而使用者却可以通过这个generator对象来达到控制函数执行的目的。就比如说这个sayHello函数,我可以在需要的时候,执行a.next()方法,来执行函数的内部逻辑。第一次执行a.next(),函数开始执行,直到它遇到yield指令,它会执行yield之后的表达式,并返回一个值,然后中断函数的运行。因此,我们看到,第一次执行a.next()后,函数输出了"before say"和"hello!"。需要说明的是,每次执行完next函数之后,都会返回一个对象:
Object {value: undefined, done: false}
这个返回值有两个属性:value和done,generator对象通过这个返回值来告诉外界函数的执行情况。value的值是yield之后的表达式的值,done则是函数执行的状态,如果函数未执行完,则其值为false,否则是true。在sayHello中,yield之后是console语句,因此返回的对象中value为undefined。
这个时候,我们再次调用a.next(),程序输出"end say"。next函数的返回值变成这样:
Object {value: undefined, done: true}
可以发现done的值变为了true,因为函数已经执行完了。
Generator可以被用来作迭代器。
首先了解一下迭代器。在ES6规范中,新增了两个协议:可迭代协议和迭代器协议。在迭代器协议中指明,一个实现了next方法并且该方法的返回值有done和value两个属性的对象,可以被当做迭代器。这些要求正好符合我们的Generator对象。举一个被当做迭代器使用的例子:
function *range(start, end){ for (let i = start; i < end; i++) { yield i; } } var a = range(0, 10); // 输出0...9 for (let i of a) { console.log(i); }
其实道理是一样的,Generator把程序的控制权交给了外部,哪里调用next,程序就在哪里执行。可想而知for...of的实现原理也一定是在内部循环执行了next方法,直到返回值的done属性变成true才停止。
为什么中间件必须是个Generator function?了解了Generator,回头再去看那段官方文档上搬来的代码。
var koa = require("koa"); var app = koa(); // x-response-time app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; this.set("X-Response-Time", ms + "ms"); }); // logger app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log("%s %s - %s", this.method, this.url, ms); }); // response app.use(function *(){ this.body = "Hello World"; }); app.listen(3000);
我们来分析代码。app.use将一个个中间件放入middlewares数组中,而app.listen启动了一个3000端口来监听http服务。实际上app.listen这个方法,底层是这样实现的:
var http = require("http"); var koa = require("koa"); var app = koa(); http.createServer(app.callback()).listen(3000);
这样你就明白了,当请求来临时,会触发在createServer时注册的回调函数(app.callback()的返回值),这个回调函数的执行其实就引发了一连串的中间件的执行。
先说结果,在探索原理。
middlewares数组中的这些中间件顺序执行,先开始进入第一个中间件 —— x-response-time,遇到yield中断执行,转而进入第二个中间件 —— logger,同样遇到yield中断执行,进入第三个中间件 —— response,这次没有遇到yield,第三个中间件执行完毕,页面输出"Hello World",done的值变为true。这个时候,再返回去执行第二个中间件刚刚中断的地方,直到第二个中间件的done也变为true,返回第一个中间件刚刚中断的位置。
是不是很神奇?这些中间件就像洋葱一样,一层一层的深入进去,又一层一层的走出来。
那么Koa是如何实现这般神奇的流程控制的呢?
Koa内部依赖了一个叫co的流程控制库。
首先,Koa实现了一个叫Koa-compose的中间件,这个中间件用来将middlewares中的所有中间件串联起来。其实现代码如下:
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; } } /** * Noop. * * @api private */ function *noop(){}
compose函数会返回一个能将众多中间件串联起来的Generator函数。这个函数从最后一个中间件开始执行,将生成的Generator对象扔给它的上一个中间件,依次类推,直到第一个中间件。这个结构真的很像一颗洋葱,从最后一个中间件开始,一层一层往上面包。
这样生成一个Generator对象之后,Koa把它交给了co这个流程控制库。co其实是个很抽象的东西。为了理解它的原理,我们可以先思考一下,如果把这个Generator对象交给我们,我们怎么类似于实现刚刚那个图所展示的效果?
从洋葱的最外层皮开始往里剥。执行第一次.next()函数,第一层中间件yield之前的程序执行完毕,通过yield next,我们拿到了第二层中间件的Generator对象。这个时候怎么办呢?按照刚刚那幅图,第一层中间件,必须要等到第二层中间件的done状态变为true之后,才可以继续执行之后的程序,即只有在第二层中间件的done状态变为true之后,才能再次执行第一层中间件Generator对象的.next()函数。同样的,之后所有的中间件都要重复这样的过程,第一层等待第二层,第二层等待第三层......那么当状态改变的时候,是不是应该有个人来通知我们?对,这个时候Promise就该出场了。
co将每个中间件.next()的运行结果的value属性都封装成一个Promise,在其done状态变为true时,resolve()这个Promise,对于洋葱里面的部分,每一层resolve之后,都会触发上一层中间件的.next()函数,并检查其状态。直到洋葱的最外面一层也resolve了,控制权就交还给Koa,而Koa会在这个时候,发起response。
co的大体思想就是这样,如果想继续深入,可以去看co的源码,自己实现一下应该也不会太难。
理解了洋葱模型,就不难明白,yield和Promise在其中所起的作用了。
关于Koa关于Koa,还有太多值得拿出来讨论的话题,我现在只是对Koa1.x中对Generator的使用做了一次整理,别的话题就慢慢再讨论吧。
最后,如果你有什么建议,欢迎不吝赐教~
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88264.html
摘要:到此为止,我们就基本讲清楚了中的中间件洋葱模型是如何自动执行的。 koa被认为是第二代web后端开发框架,相比于前代express而言,其最大的特色无疑就是解决了回调金字塔的问题,让异步的写法更加的简洁。在使用koa的过程中,其实一直比较好奇koa内部的实现机理。最近终于有空,比较深入的研究了一下koa一些原理,在这里会写一系列文章来记录一下我的学习心得和理解。 在我看来,koa最核心...
摘要:返回后,代表操作已完成,记录结束时间并输出。从零组装因为对的学习和使用,知道了自己对于后台框架的真实需求。所以这回决定不用之内的工具,而是自己从零开始,组装一个适合自己的框架。就是去和上,寻找一个一个的包并组装在一起了而已。 起因 作为一个前端,Node.js算是必备知识之一。同时因为自己需要做一些后台性的工作,或者完成一个小型应用。所以学习了Node的Express框架,用于辅助和加...
摘要:现在我们从实现一个简易的方法开始探索其中的机制。其中内部的可以将上一个的返回值传递给外部。一言以蔽之实现了递归调用的方法。当执行到的中间件没有时并且返回的为时逆序执行。 本文发布在github.com/ssssyoki,欢迎star,issues共同交流。 Koa是基于Node.js的下一代web开发框架,相比Express更轻,源码只有几百行。与传统的中间件不同,在Koa 1.x中采...
摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天阅读了的基础,和中间件的基础。的前端乐园原文链接源码阅读笔记服务器启动与请求处理 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起因 前两天阅读了K...
阅读 1280·2021-11-16 11:45
阅读 2216·2021-11-02 14:40
阅读 3821·2021-09-24 10:25
阅读 3014·2019-08-30 12:45
阅读 1178·2019-08-29 18:39
阅读 2455·2019-08-29 12:32
阅读 1533·2019-08-26 10:45
阅读 1898·2019-08-23 17:01