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

理解函数组合(compose)和中间件实现

时间:2023-03-27 16:47:25 JavaScript

什么是函数组合?函数组合可以理解为将一系列简单的基本函数组合成可以完成复杂任务的函数的过程;这些基本函数需要接受一个参数并返回数据,这个数据应该是另一个未知程序的输入;使用compose函数看一个只接受两个参数的compose函数,然后改进compose函数varcompose=function(f,g){returnfunction(x){returnf(g(x));};};这就是函数组合(compose),f和g都是函数,x是通过它们之间的“管道”传递的值。我们可以组合函数来产生一个全新的函数:vartoUpperCase=function(str){returnstr.toUpperCase();};varexclaim=function(str){returnstr+'!';};varshout=compose(exclaim,toUpperCase);shout('helloworld');//你好,世界!让代码从右到左而不是从里到外运行//获取列表中的第一个元素varhead=function(arr){returnarr[0];};//反向列表varreverse=function(arr){returnarr.reduce(function(cur,next){return[next].concat(cur);},[]);}varlast=compose(head,撤销);last(['苹果','香蕉','橙子']);//橙色可以看出compose的数据流向是从右到左,从Execution从右到左更能体现combination在数学上的意义;combinationsatisfiesassociativelaw//associativity(关联性)varassociative=compose(f,compose(g,h))==compose(compose(f,g),h);//true这个特性就是关联律,也就是说将g和h或f和g分组并不重要。所以,如果我们想将一个字符串大写,我们可以这样写:compose(toUpperCase,compose(head,reverse));//或者compose(compose(toUpperCase,head),reverse);如何使用组合假设我们有这样一个需求:给你一个数组来展平和去重vararr=[1,2,[2,10,0,[5,6,4,[7,1]]]];varflattenAndUnique=function(arr){while(arr.some(Array.isArray)){arr=[].concat(...arr)}returnArray.from(newSet(arr));}flattenAndUnique(arr)复制代码;//[1,2,10,0,5,6,4,7]这段代码没有问题,但是现在有新的需求,需要排序。为了达到这个目的,我们需要改变我们之前封装的功能,这实际上是打破了设计模式中的开闭原则。开闭原则:软件中的对象(类、模块、函数等)应该对扩展开放,对修改关闭。那么在需求没变,数组还是扁平化去重的情况下,结合的思路怎么写呢?原来的需求,我们可以这样实现:vararr=[1,2,[2,10,0,[5,6,4,[7,1]]]];varflatten=function(arr){while(arr.some(Array.isArray)){arr=[].concat(...arr)}returnarr;};varunique=function(arr){returnArray.from(新设置(arr));};varflattenAndUnique=compose(unique,flatten);varresult=flattenAndUnique(arr)//[1,2,10,0,5,6,4,7]然后当我们添加一个新的需求排序时,我们不不需要修改之前的封装函数://...//添加数组排序方法varsort=function(arr){returnarr.sort(function(a,b){returna-b;})};varflattenAndUniqueAndSort=compose(sort,unique,flatten);varresult=flattenAndUniqueAndSort(arr)//[0,1,2,4,5,6,7,10]我们可以看到,在更改需求的时候,并没有打破之前的封装代码,只是增加了函数功能,然后重新组合功能。我们假设现在需求已经改变,需要求和,所以我们可以这样实现://addvargetSum=function(arr){returnarr.reduce(function(cur,next){returncur+下一个},0);};varflattenAndUniqueAndSum=compose(getSum,unique,flatten);varresult=flattenAndUniqueAndSum(arr)//35从这个例子可以看出,通过组合一些单一功能的函数,然后组合复杂的函数,不仅仅是代码逻辑更加清晰,也给维护带来了极大的方便。实现Composition从上面我们可以看出,compose接收了几个函数作为参数,并返回了一个新的函数。执行新函数时,传入compose的函数从右到左依次执行,每个函数的执行结果作为下一个函数的输入,直到最后一个函数的输出作为最终的输出结果varcompose=function(f,g){returnfunction(x){returnf(g(x));};};这样只能接受两个参数,但是显然我们需要考虑的是compose接收的参数个数是不确定的,所以我们可以使用reduceRight写一个通用版本//reduceRightimplementsconstcompose=(...fns)=>(value)=>fns.reduceRight((acc,fn)=>fn(acc),value)如果我们要让最左边的函数先执行,我们需要改变数据流的方向;从左到右处理数据流的过程称为管道或序列)=>(value)=>fns.reduce((acc,fn)=>fn(acc),value)compose实际用例lodashlodash/flowRight.js在masterlodash/lodash(github.com)functionflowRight(...funcs){returnflow(...funcs.reverse());}//flow.jsfunctionflow(...funcs){constlength=funcs.lengthletindex=lengthwhile(index--){if(typeoffuncs[index]!=='function'){thrownewTypeError('Expectedafunction')}}returnfunction(...args){让索引=0让结果=长度?funcs[index].apply(this,args):args[0]while(++index&l吨;length){result=funcs[index].call(this,result)}returnresult}}underscore.jsunderscore/underscore.js在masterjashkenas/underscoreGitHubfunctioncompose(){varargs=arguments;varstart=args.length-1;返回函数(){vari=开始;varresult=args[i].apply(this,arguments);while(i--)result=args[i].call(this,result);返回结果;}}Koa2中间件koa2使用的异步方案是async/awaitconstKoa=require('koa');constapp=newKoa();app.use(async(ctx,next)=>{ctx.body='Hello世界';console.log(1);next();console.log(4);});app.use(async(ctx,next)=>{console.log(2);next();console.日志(3);});app.listen(3000);运行http://127.0.0.1:3000/会输出1234原理koa通过内部数组队列中的use函数Use(fn)将所有中间件推送到一个this.middlewares{if(typeoffn!=='function')thrownewTypeError('middlewaremustbeafunction!');//判断中间件函数是否为生成器generatorsif(isGeneratorFunction(fn)){deprecate('对生成器的支持将在v3中删除。'+'请参阅文档以获取有关如何转换旧中间件的示例'+'https://github.com/koajs/koa/blob/master/docs/迁移.md');//如果是generators函数,会转为async/awaitfn=convert(fn);}debug('使用%s',fn._name||fn.name||'-');//使用中间件数组存放中间件this.middleware.push(fn);returnthis;}koa中间件的执行过程主要是通过koa-compose中的compose函数来完成洋葱模型原理:所有的中间件都是顺序执行的,每个中间件第一次执行一个中间件,遇到next()时,控制将传递给下一个中间件。下一个中间件的next参数,当上一个中间件执行完后,控制会反转,开始往回走。所有中间件在执行前都残留有未执行的代码;当所有的中间件最终执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async函数,而async函数执行完后,会返回一个Promise对象Promise,这样我们就可以同步所有中间件的异步执行,然后通过then执行响应函数和错误处理函数ofmiddleware){if(typeoffn!=='function')thrownewTypeError('Middlewaremustbecomposedoffunctions!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//最后调用的中间件#//计数器,用来判断是否执行到最后letindex=-1//从第一个中间件方法开始执行returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()多次调用'))index=iletfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)returnPromise.resolve()try{//递归调用nextMiddlewarereturnPromise.resolve(fn(context,dispatch.bind(null,i+1)))}catch(err){returnPromise.reject(err)}}}}下图是网上找到的redux中间件reduxcomposeexport默认函数compose(...funcs:Function[]){if(funcs.length===0){//推断参数类型,因此它可用于后续推理return(arg:T)=>arg}if(funcs.length===1){returnfuncs[0]}returnfuncs.reduce((a,b)=>(...args:any)=>a(b(...args)))}参考:JS函数式编程指南https://juejin.cn/post/684490..……