最近想到一道我觉得很有意思的面试题,如何实现一个compose函数。函数接收几个参数,都是Function类型的,右边函数的执行结果会作为左边函数的参数被调用。compose(arg=>`${arg}%`,arg=>arg.toFixed(2),arg=>arg+10)(5)//15.00%compose(arg=>arg.toFixed(2),arg=>arg+10)(5)//15.00compose(arg=>arg+10)(5)//15的执行结果如上代码,有兴趣的同学可以先自己实现再看跟进。1.0实现方案的大致思路是:获取所有参数,调用最后一个函数,接收返回值。如果没有后续函数,则返回数据。如果有,则将返回值放入下一个函数中执行。所以在这种情况下,使用递归实现会更清晰返回值//如果还有多个Function,将返回值放入下一个函数中执行//如果没有后续,直接returnreturnfuncs.length?exec(result):result}}这样,我们就实现了上面的compose功能。真是喜闻乐见,喜闻乐见。这篇文章结束了。好吧,如果现实生活中的需求开发能如此直截了当、朴实无华就好了。然而,产品总会来,需求总会变。2.0需求变化我们现在有如下需求,功能需要支持Promise对象,并且必须兼容普通功能。示例代码如下://排版修改方便阅读compose(arg=>newPromise((resolve,reject)=>setTimeout(_=>resolve(arg.toFixed(2)),1000)),arg=>arg+10)(5).then(data=>{console.log(data)//15.00})我们有以下代码调用,它向Fixed函数的调用添加了1000毫秒的延迟。让用户感觉这个函数执行起来很慢,方便下一步优化。因此,我们需要修改compose函数。我们之前的代码只能支持普通功能的处理。现在因为增加了Promise对象,我们需要做如下修改:首先,将异步函数改为同步函数,除了readFile/readFileSync,不存在。所以,最简单的办法就是将普通函数改成异步函数,也就是把一层Promise外包给普通函数。functioncompose(...funcs){returnfunctionexec(arg){returnnewPromise((resolve,reject)=>{letfunc=funcs.pop()letresult=promiseify(func(arg))//执行函数,获取返回值,并将返回值转化为`Promise`对象//注册`Promise`的`then`事件,准备在里面执行下一次函数//判断函数中是否还有函数后续,如果有,继续执行//如果没有,直接返回结果result.then(data=>funcs.length?exec(data).then(resolve).catch(reject):resolve(data)).catch(reject)})}}//判断参数是否为`Promise`functionisPromise(pro){returnproinstanceofPromise}//将参数转换为`Promise`functionpromiseify(pro){//如果结果为`Promise`,直接返回if(isPromise(pro))returnpro//如果结果是这些基本类型,说明是一个普通的函数//我们用`Promise.resolve`包裹起来if(['string','number','regexp','object'].includes(typeofpro))returnPromise.resolve(pro)}我们对compose代码的改动主要集中在这些地方:将compose的返回值改成一个Promise对象,这是不可避免的,因为里面可能包含Promise参数,所以我们必须返回一个Promise对象,将每次函数执行的返回值包装到一个Promise对象中,以便统一返回值。处理函数的返回值,监听then和catch,将resolve和reject传递给它。3.0旗舰版现在,我们有一个新的需求,我们想在某些函数执行的过程中跳过部分代码,先执行后面的函数,等到后面的函数执行完,然后拿到返回值再执行剩下的代码:compose(data=>newPromise((resolve,reject)=>resolve(data+2.5)),data=>newPromise((resolve,reject)=>resolve(data+2.5)),asyncfunctionc(data,next){//async/await是Promise语法糖,不用赘述data+=10//value+10letresult=awaitnext(data)//先执行后续代码result-=5//value-5returnresult},(data,next)=>newPromise((resolve,reject)=>{next(data).then(data=>{data=data/100//将值除以100limitpercentresolve(`${data}%`)}).catch(reject)//先执行后续代码}),functiond(data){returndata+20})(15).then(console.log)//0.45%拿到需求后,陷入了沉思。..按顺序执行代码,突然变成这只鸟,随时可能跳转到后面的函数。所以我们分析这个新需求的效果:我们在函数执行到一半的时候执行next,next的返回值就是后续函数的执行返回值。也就是说,我们在next中处理,直接调用队列中的next函数即可;然后监听then和catch回调,然后我们就可以在当前函数中获取返回值;得到返回值后,我们就可以执行我们后面的代码了。那么他的实现也很简单,我们只需要修改下面的代码就可以完成操作://这里会强制调用`exec`并传入参数//而`exec`的执行就是`funcs`中的另一个函数集合从队列中取出promiseify(func(arg,arg=>exec(arg)))也就是说我们会提前执行下一个函数,下一个函数的then事件注册在我们的里面当前函数,当我们得到返回值后,就可以进行后续处理了。我们所有的功能都存储在一个队列中。我们提前执行完函数之后,后面的执行就不会再出现了。避免一个函数被重复执行的问题。如果你已经看懂了这里的一切,恭喜你已经看懂了实现koajs的核心代码:中间件的实现方法,洋葱模型,整个功能现在一定是洋葱味儿了。参考资料koa-compose相关示例代码仓库1.0,常用函数2.0,Promise函数3.0,支持onion模型
