最近看了koa2的源码,搞清楚了架构设计和使用的第三方库。本系列将分为3篇文章,分别介绍koa的架构设计和3个核心库,最后手工实现一个简单的koa。这是本系列的第二部分,关于3个核心库的原理。一系列专注于前端和算法的干货分享,欢迎关注(???):《微信公众号:新坛博客》|新坛网|githubis-generator-function:判断generatorkoa2推荐使用asyncfunction,koa1推荐的是generator。为了兼容,koa2在调用use添加中间件的时候会判断是否是generator。如果是,则使用隐蔽库转换为异步函数。判断是否是生成器的逻辑写在is-generator-function库中。逻辑很简单,通过判断Object.prototype.toString.call的返回结果即可:function*say(){}Object.prototype.toString.call(say);//Output:[objectGeneratorFunction]delegates:propertyproxydelegates和koa一样,这个库来自大师TJ。它的作用是属性代理。这个代理库常用的方法有getter、setter、method和access。用法假设一个对象目标已经准备好了。为了方便访问request属性的内容就可以了,请求委托:constdelegates=require("delegates");consttarget={request:{name:"xintan",say:function(){console.log("Hello");}}};delegates(target,"request").getter("name").setter("name").method("say");代理之后,访问request会更方便:console.log(target.name);//xintantarget.name="xintan!!!";console.log(target.name);//心坛!!!target.say();//Hello通过调用对象的__defineSetter__和__defineGetter__实现setter和getter方法。下面是单独取出来的逻辑:/***@param{Object}proto代理对象*@param{String}property代理对象上的代理属性*@param{String}name*/functionmyDelegates(proto,property,名称){proto.__defineGetter__(名称,函数(){returnproto[property][name];});proto.__defineSetter__(name,function(val){return(proto[property][name]=val);});}myDelegates(target,"request","name");console.log(target.name);//xintanttarget.name="xintan!!!";console.log(target.name);//新坛!!!一开始我的想法比较简单,就是直接让proto[name]=proto[property][name]。但是这样有一个无法补救的缺点,就是如果proto[property][name]后面发生变化,proto[name]就无法获取到最新的值。对于method方法,实现是在对象上新建一个属性,属性值是一个函数。该函数调用代理目标的函数。下面是单独取出来的逻辑:/****@param{Object}proto代理对象*@param{String}property代理对象上的代理属性*@param{String}方法函数名*/functionmyDelegates(proto,property,method){proto[method]=function(){returnproto[property][method].apply(proto[property],arguments);};}myDelegates(target,"request","say");target.say();//因为Hello是一个“代理”,所以这里不能修改上下文。proto[property][method]的context是proto[property],需要apply重新指定。koa中也有属性的访问方法代理。这个方法就是getter和setter一起写的语法糖。koa-compose:onionmodel仿真洋葱模型koa最神奇的地方就是大名鼎鼎的“洋葱模型”。以至于我在开发koa中间件的时候,一直有一个神奇的方法。我常常想,这里awaitnext(),执行完之后,中间件会回来继续执行未执行的逻辑。这段逻辑封装在核心库koa-compose中。源码也很简单,包括各种注释不到50行。为了方便解释和理解,我去掉了一些检查意外情况的代码:functioncompose(middleware){returnfunction(context){returndispatch(0);functiondispatch(i){letfn=middleware[i];尝试{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}catch(err){returnPromise.reject(err);}}};}中间件是开发者自定义的中间件处理逻辑。为了便于说明,我准备了2个中间件函数:constmiddleware=[async(ctx,next)=>{console.log("a");等待下一个();控制台日志(“c”);},async(ctx,next)=>{console.log("b");}];现在,为了模拟在koa中调用compose函数,我们希望程序的输出是:abc(就像使用koa一样)。只需运行以下代码:constfns=compose(middleware);fns();ok,一个不考虑异常情况的洋葱模型已经模拟出来了。为什么?为什么会有洋葱穿透的效果?回到上面的compose函数,闭包写法返回一个新的函数,其实返回的是内部定义的dispatch函数。其中,参数的含义为:i:当前执行的中间件在所有中间件上下文中的下标:上下文。这样我们就可以在各个中间件中访问到当前请求的信息了。上面的测试用例中,fns其实就是dispatch(0)。在dispatch函数中,通过参数i获取要运行的中间件fn。然后,将当前请求的上下文和dispatch处理的下一个中间件(next)传递给当前中间件。对应的代码段为:returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));那么,在中间件中执行awaitnext()其实就是在执行:awaitdispatch.bind(null,i+1)。所以看起来当前的中间件会停止自己的逻辑,先处理下一个中间件的逻辑。因为每次dispatch都会返回一个新的Promise。所以async会一直等到Promise状态改变了再回来继续执行自己的逻辑。async/await重写最后在不考虑koa上下文的情况下,用async/await提取了compose函数:functioncompose(middleware){returndispatch(0);asyncfunctiondispatch(i){letfn=middleware[i];尝试{awaitfn(dispatch.bind(null,i+1));}catch(err){返回错误;}}}以下是它的使用方式:constmiddleware=[asyncnext=>{console.log("a");等待下一个();控制台日志(“c”);},asyncnext=>{console.log("b");}];撰写(中间件);//输出abc最后一段代码希望能帮助理解!一系列专注于前端和算法的干货分享,欢迎关注(???)
