浅谈compose

243 阅读5分钟

「这是我参与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)的结果。