最近想到了一个自认为很有意思的面试题
如何实现一个compose
函数。
函数接收数个参数,参数均为Function
类型,右侧函数的执行结果将作为左侧函数执行的参数来调用。
1 compose(arg => `${arg}%`, arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00% 2 compose(arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00 3 compose(arg => arg + 10)(5) // 15
执行结果如上述代码,有兴趣的同学可以先自己实现一下再来看后续的。
1.0实现方案
大致的思路为:
- 获取所有的参数
- 调用最后一个函数,并接收返回值
- 如果没有后续的函数,返回数据,如果有,将返回值放入下一个函数中执行
所以这种情况用递归来实现会比较清晰一些
1 function compose (...funcs) { 2 return function exec (arg) { 3 let func = funcs.pop() 4 let result = func(arg) // 执行函数,获取返回值 5 6 // 如果后续还有函数,将返回值放入下一个函数执行 7 // 如果后续没有了,直接返回 8 return funcs.length ? exec(result) : result 9 } 10 }
这样,我们就实现了上述的compose
函数。
真是可喜可贺,可喜可贺。
本文完。
好了,如果现实生活中开发做需求也是如此爽快不做作就好了,但是,产品总是会来的,需求总是会改的。
2.0需求变更
我们现在有如下要求,函数需要支持Promise
对象,而且要兼容普通函数的方式。
示例代码如下:
1 // 为方便阅读修改的排版 2 compose( 3 arg => new Promise((resolve, reject) => 4 setTimeout(_ => 5 resolve(arg.toFixed(2)), 6 1000 7 ) 8 ), 9 arg => arg + 10 10 )(5).then(data => { 11 console.log(data) // 15.00 12 })
我们有如下代码调用,对toFixed
函数的调用添加1000ms
的延迟。让用户觉得这个函数执行很慢,方便下次优化
所以,我们就需要去修改compose
函数了。
我们之前的代码只能支持普通函数的处理,现在因为添加了Promise
对象的原因,所以我们要进行如下修改:
首先,异步函数改为同步函数是不存在的readFile/readFileSync
这类除外。
所以,最简单的方式就是,我们将普通函数改为异步函数,也就是在普通函数外包一层Promise
。
1 function compose (...funcs) { 2 return function exec (arg) { 3 return new Promise((resolve, reject) => { 4 let func = funcs.pop() 5 6 let result = promiseify(func(arg)) // 执行函数,获取返回值,并将返回值转换为`Promise`对象 7 8 // 注册`Promise`的`then`事件,并在里边进行下一次函数执行的准备 9 // 判断后续是否还存在函数,如果有,继续执行 10 // 如果没有,直接返回结果 11 result.then(data => funcs.length ? 12 exec(data).then(resolve).catch(reject) : 13 resolve(data) 14 ).catch(reject) 15 }) 16 } 17 } 18 19 // 判断参数是否为`Promise` 20 function isPromise (pro) { 21 return pro instanceof Promise 22 } 23 24 // 将参数转换为`Promise` 25 function promiseify (pro) { 26 // 如果结果为`Promise`,直接返回 27 if (isPromise(pro)) return pro 28 // 如果结果为这些基本类型,说明是普通函数 29 // 我们给他包一层`Promise.resolve` 30 if (['string', 'number', 'regexp', 'object'].includes(typeof pro)) return Promise.resolve(pro) 31 }
我们针对compose
代码的改动主要是集中在这几处:
- 将
compose
的返回值改为了Promise
对象,这个是必然的,因为内部可能会包含Promise
参数,所以我们一定要返回一个Promise
对象 - 将各个函数执行的返回值包装为了
Promise
对象,为了统一返回值。 - 处理函数返回值,监听
then
和catch
、并将resolve
和reject
传递了过去。
3.0终极版
现在,我们又得到了一个新的需求,我们想要在其中某些函数执行中跳过部分代码,先执行后续的函数,等到后续函数执行完后,再拿到返回值执行剩余的代码:
1 compose( 2 data => new Promise((resolve, reject) => resolve(data + 2.5)), 3 data => new Promise((resolve, reject) => resolve(data + 2.5)), 4 async function c (data, next) { // async/await为Promise语法糖,不赘述 5 data += 10 // 数值 + 10 6 let result = await next(data) // 先执行后续的代码 7 8 result -= 5 // 数值 - 5 9 10 return result 11 }, 12 (data, next) => new Promise((resolve, reject) => { 13 next(data).then(data => { 14 data = data / 100 // 将数值除以100限制百分比 15 resolve(`${data}%`) 16 }).catch(reject) // 先执行后续的代码 17 }), 18 function d (data) { return data + 20 } 19 )(15).then(console.log) // 0.45%
拿到需求后,陷入沉思。。。
好好地顺序执行代码,突然就变成了这个鸟样,随时可能会跳到后边的函数去。
所以我们分析这个新需求的效果:
我们在函数执行到一半时,执行了next
,next
的返回值为后续函数的执行返回值。
也就是说,我们在next
中处理,直接调用队列中的下一个函数即可;
然后监听then
和catch
回调,即可在当前函数中获取到返回值;
拿到返回值后就可以执行我们后续的代码。
然后他的实现呢,也是非常的简单,我们只需要修改如下代码即可完成操作:
1 // 在这里会强行调用`exec`并传入参数 2 // 而`exec`的执行,则意味着`funcs`集合中又一个函数被从队列中取出来 3 promiseify(func(arg, arg => exec(arg)))
也就是说,我们会提前执行下一个函数,而且下一个函数的then
事件注册是在我们当前函数内部的,当我们拿到返回值后,就可以进行后续的处理了。
而我们所有的函数是存放在一个队列里的,在我们提前执行完毕该函数后,后续的执行也就不会再出现了。避免了一个函数被重复执行的问题。
如果看到这里已经很明白了,那么恭喜,你已经了解了实现koajs
最核心的代码:
中间件的实现方式、洋葱模型
想必现在整个函数周遭散发着洋葱
的味道。