「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
什么是compose函数
compose就是执行一系列函数的函数,这样子说可能有点绕口,看看下面的例子:
compose 就是执行一系列的任务(函数),比如有以下任务队列
let tasks = [step1, step2, step3, step4]
每一个 step 都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个 compose
compose 在函数式编程中是一个很重要的工具函数,在这里实现的 compose 有三点说明
- 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
- 执行顺序的自右向左的
- 所有函数的执行都是同步的
还是用一个例子来说,比如有以下几个函数
let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
let step2 = (val) => val + 2
let step3 = (val) => val + 3
let step4 = (val) => val + 4
这几个函数组成一个任务队列
steps = [step4, step3, step2, init]
使用 compose 组合这个队列并执行
let composeFunc = compose(...steps)
console.log(composeFunc(1, 2, 3))
执行过程
6 -> 6 + 2 = 8 -> 8 + 3 = 11 -> 11 + 4 = 15
所以流程就是从 init 自右到左依次执行,下一个任务的参数是上一个任务的返回结果,并且任务都是同步的,这样就能保证任务可以按照有序的方向和有序的时间执行。
实现compose
其实知道了compose函数是什么,有什么用之后,就大概知道怎么实现了,其实实现compose函数的方法有很多种,这里只说比较好理解的一种,其他方法可以查看 segmentfault.com/a/119000001…。其实不难发现我们是先要从函数队列的后部开始执行的,然后在把返回值给上一个函数使用。
function compose(...funcs){
let count = funcs.length - 1
let res
return function fn(...args){
res = funcs[count].apply(this,args)
if(count <= 0){
// 必须将count还原,不然在多次调用之后,会保留之前的count
count = funcs.length - 1
return res
}
count--
return fn(res)
}
}
我们来上测试代码
let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
let step2 = (val) => val + 2
let step3 = (val) => val + 3
let step4 = (val) => val + 4
let steps = [step4, step3, step2, init]
let composeFunc = compose(...steps)
let composeFunc2 = compose(...steps)
console.log(composeFunc(1, 2, 3)) // 15
console.log(composeFunc(1, 2, 3)) // 15
经过测试代码后,发现这个compose能够得到我们预期的结果,我们再逐行剖析一下这个函数的实现吧!
首先我们是让获取到了函数数组的最后一位,并把他赋值给了count,这个count就是我们用来记录当前执行的函数。然后我们其实返回的是一个函数,这里运用了闭包的概念,其实像很多东西的实现都有用到了闭包,比如函数柯里化,bind的实现,防抖,节流等等。这里使用闭包,主要是为了保存count和funcs,使得我们在进行函数的递归时能够访问的一直都是原来那个funcs,获取和改变的都是原来那个count。
fn函数内部先是调用了当前函数,也就是我们利用count记录的函数,然后调用这个函数,并保存他的结果,以作为下一次调用的参数,紧接着,判断如果当前count为0,则是返回res,同时之前还要初始化count为参数length-1,这是因为闭包的副作用产生的,我们要是连续调用两次compose的函数,count不初始化的话,那么,我们第二次调用,只会调用第一个函数,这是因为由于闭包的存在,此时的count一直保存着,上一次调用时,count已经为0了,因此第二次调用时count依然为0。所以在这里,我们必须将count设置回函数列表的最后一个索引。如果count不为0,往下执行,count减一,即前往上个函数,之后再将结果传入作为下次调用的参数。因为res只有一个值,这也就是我们所示的,第一个执行的函数可以多元,后面执行的函数只能单元的原因。
redux中compose的实现
reduxcompose的源码在/src/compose.js中
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
看到这样的实现,不得不说一句妙啊!!!
这里是用到了reduce的实现方式,至于reduce的用法,请看 这里
这里我们依旧通过之前的例子来解释
let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
let step2 = (val) => val + 2
let step3 = (val) => val + 3
let step4 = (val) => val + 4
let steps = [step4, step3, step2, init]
let composeFunc = compose(...steps)
console.log(composeFunc(1, 2, 3))
我们一步一步来看
首先是两个判断,如果没有传入函数,则返回一个输入什么返回什么的函数;如果传入的只有一个函数,那么就直接返回那么函数。
接下来reduce的使用才是重点,reduce没有传入默认参数,也就是说它会以第一个参数为默认参数,用我们的例子就是:
第一步返回的函数是:
let a = (...args) => step4(step3(...args))
为了方便描述,这个结果先记录为a,下同。
第二步返回的函数则就是:
let b = (...args) => a(step2(...args))
// 也就是(...args)=>step4(step3(step2(...args))),之前的a函数的args被替换成了step2(...args)这个函数了
第三步其实也是差不多的
let c = (...args) => b(init(...args))
// 执行到这里,c就是我们返回的最终函数了,即是(...args)=>step4(step3(step2(init(...args))))
这样子的话就可以达到compose函数的效果啦,因为你要执行step4,你就要得到step3的结果,因为他是step4的参数,以此类推,就是要先知道init(...args)的结果。