记一次compose异步管道函数

465 阅读4分钟

wallhaven-72wzq9.png 本文已参与「新人创作礼」活动,一起开启掘金创作之路。。。

方法链

在函数式编程中,经常使用的有2种方式连接一系列函数,一个是方法链,一个是管道。在方法链中,我们应该都用过jquery的命令式编程,他可以一条链下去.返回的是自身对象,同时也有像lodash提供的chain函数。他使用起来可能长这样:

_.chain(names)
  .filter(isValid)
  .map(s => s.replace('xx',''))
  .uniq()
  .sort()
  .value();

比较命令式代码,这的确是一个能够极大提高代码可读性的语法。然而,它与方法所属的对象紧紧地耦合在一起,限制链中可以使用的方法数量,也就限制了代码的表现力。这样就只能够使用由lodash提供的操作,而无法与其他函数库或自定义的函数连接在一起了。这是最大的缺点。

函数管道化

而另一种经常使用的管道函数,使得任何函数可以组合,它表现得更加灵活。它是松散结合的有向函数序列,一个函数的输出会作为下一个函数的输入。

image.png

每个输入到管道的数据,都会经过管道函数进行转化,输出为适合下一个管道类型的参数,所以说接收函数必须声明至少一个参数才能处理上一个函数的返回值,这也算是一个条件。

compose同步实现

我们接用reduce来实现下compose:

// fn1(fn2(...fn))
const compose = (...fn) =>
  fn.reduce((acc, cur) => (...args) => acc(cur(...args)));

这里我们就实现了一个同步版本的compose,要记住因为使用reduce做的,它的执行管道函数的方向是从右到左,如果要正向,可以使用reduceRight。

compose异步实现

通常我们的管道他可能不是同步代码,他是异步代码,这个时候,我们如果继续使用上面的compose他会解构不出来promise,因为上面的设计不是针对异步函数来的,这个时候我们就要改写下compose了,就连ramda里面的compose,他也是设计成同步的。

const asyncCompose = (...fn) =>
  fn.reduce((acc, cur) => {
    return async (...args) => acc(await cur(...args));
  });

在之前,我不知道该如何解构promise出来,都是在每个fn里return await 数据,但是这样根本就没有意义,因为真正的执行是在compose里,还记得我们说的条件吗?每个输入到管道的数据,都会经过管道函数进行转化,输出为适合下一个管道类型的参数。也就是说我们真正解构的操作要在入参那里,参数可以await的!!!

这个时候,我们就可以使用异步的compose去处理一些异步函数的管道化了,但是还有一个注意的点,compose是一个惰性函数,什么意思呢?就是先组合,使用的时候再调用组合好的函数执行,这样可以进行到一个性能调用,我们看看使用的例子吧:

const asyncCompose = (...fn) =>
  fn.reduce((acc, cur) => {
    return async (...args) => acc(await cur(...args));
  });


const addTen = async (a) => {
  return a + 10
}

const getNum = async () => {
  return Promise.resolve(10).then((res) => {
    return res
  })
}

const lazy = asyncCompose(addTen, getNum)

const fn = async () => {
  const res = await lazy()
  console.log(res); //20
}

fn()

还有一个注意的点是:最终执行的lazy,他其实还是一个promise,所以还需要进行最终一次的解构。这也是为啥又创建了一个异步的fn。

compose类型定义

在compose函数类型定义过程中,我参考了ramdajs的pipe函数,发现他是写了暴力函数重载方式实现的,这确实可以解决掉这个场景,但是定义起来很麻烦,所以我使用了个比较简单的办法,虽然不是最优解,但也能解决掉大部分场景,除非最后一个函数的返回值是那种动态值,就没办法,不知道各位有没有更好的办法定义compose呢?

// 参数是最后一个函数的参数 返回值为第一个函数的返回值
type ComposeReturnType<T extends Array<(...args: any) => any>> = (
  ...args: Parameters<LastArrItem<T>>
) => ReturnType<FirstArrItem<T>>;

const compose = <T extends Array<(...args: any) => any>>(...fn: T): ComposeReturnType<T> =>
  fn.reduce((acc, cur) => {
    return (...args) => acc(cur(...args));
  });

总结

函数组合子,我们尽量使用纯函数去书写,这样这份功能函数,也就达到了复用性高,拓展性强的功能函数,利用组合将我们的子问题一个个组合成新的功能函数,这种写法比起方法链来说,更加灵活可重用,但可读性相对来说会差些,需要多理解这个compose的执行顺序,而且在进行代码阅读的时候,如果只针对流程组合,需要一一对应各个组合子的入参及上一个组合子的返回值来对比才知道这段代码是干什么的,所以说良好的注释及命名,很有必要,以及ts的类型推断。