compose 在前端被广泛使用,主要用于实现各种插件系统,如 koaredux 的 middleware。其实现很简短,但是却很难理解。本文尝试尽量简单的阐述其原理。 从简单的例子开始:

function fn1(num) {
  console.log('fn1', num)
  return num
}

function fn2(num) {
  console.log('fn2', num)
  return num
}

function fn3(num) {
  console.log('fn3', num)
  return num
}

function fn4(num) {
  console.log('fn4', num)
  return num
}

现在思考一个问题,我希望用相同的参数逐一调用 fn1fn4 的函数,但是我不想这样写:

fn1(1)
fn2(1)
fn3(1)
fn4(1)

我想构造一个函数 composed,使得 composed(1) 就可以实现逐一调用 fn1 到 fn4, 很自然的我们可以构造一个这样的函数,来实现我们的需求:

let composed = (num) => fn4(fn3(fn2(fn1(num))))

道理很简单,函数的参数会被先计算,然后再执行函数。

现在我们是手动生成 composed 的,但是这样太麻烦,我们是否可以写一个函数来自动生成 composed 呢?有了上面的思路,我们只要生成一个函数,在其中遍历fn1到fn4,逐一调用,不就行了:

/**
 *
 * @param funs 函数列表,例:[fn1, fn2, fn3, fn4]
 * @returns composed 函数,例:(num) => fn4(fn3(fn2(fn1(num))))
 */
function compose(funs) {
  // 返回的 composed, 必然是一个参数为 num 的函数
  return (num) => {
    let length = funs.length

    // 当前的参数,初次的参数是 num
    let ret = num

    // fn4(fn3(fn2(fn1(num)))
    for (let i=0; i<length; i++) {
      let fn = funs[i]
      ret = fn(ret)
    }
  }
}

let composed = compose([fn1, fn2, fn3, fn4])

我们再用 es6 的语法简写一版:

function compose(funs) {
  // 返回的 composed, 必然是一个参数为 num 的函数
  return (num) => funs.reduce((previous, current) => current(previous), num)
}

let composed = compose([fn1, fn2, fn3, fn4])

大家可能在一些源码里会直接看到这个版本,然后直接懵逼。

我们继续看下面这个更复杂的场景:

function fn1(next){
    console.log('fn1')
    next()
    console.log('end fn1')
}

function fn2(next){
    console.log('fn2')
    next()
    console.log('end fn2')
}

function fn3(next){
    console.log('fn3')
    next()
    console.log('end fn3')
}

function fn4(next){
    console.log('fn4')
    next()
    console.log('end fn4')
}

构建一个 compsed 函数,让其执行结果为:

fn1
fn2
fn3
fn4
end fn4
end fn3
end fn2
end fn1

即洋葱卷模型

和上面的简单例子类似,我们可以这样来设计 composed:

// 为了避免混淆,这里我们把参数名定为 oNext
let composed = (oNext) => fn1(() => fn2(() =>fn3(() => fn4(oNext))))

这个极难想到,也非常难理解

我们先定义一个 oNext 函数备用:

let oNext = () => console.log('oNext called')

执行 composed 函数,composed(oNext),即是执行 fn1(() => fn2(() =>fn3(() => fn4(oNext)))),此时 fn1 的参数是一个函数 () => fn2(() =>fn3(() => fn4(oNext))), 被当做 next 参数传入了 fn1 并执行。

function fn1(next){
    console.log('fn1')       <--- 1
    /**
     * next = () => fn2(() =>fn3(() => fn4(oNext)))
     **/
    next()                   <--- 2
    console.log('end fn1')
}

执行 fn1next 参数,即 fn2(() =>fn3(() => fn4(oNext)))

function fn2(next) {
    console.log('fn2')       <--- 3
    /**
     * next = () =>fn3(() => fn4(oNext))
     **/
    next()                   <--- 4
    console.log('end fn2')
}

依次类推:

function fn3(next){
    console.log('fn3')       <--- 5
    /**
     * next = () => fn4(oNext)
     **/
    next()                   <--- 6
    console.log('end fn3')
}

function fn4(next){
    console.log('fn4')       <--- 7
    /**
     * next = oNext
     **/
    next()                   <--- 8 // 这里执行的是 oNext, 所以打印出的是 oNext called
    console.log('end fn4')
}

fn4 的 next 参数就是 oNext 了,所以没有进一步的调用栈了,fn4 继续执行返回

function fn4(next){
    console.log('fn4')       <--- 7
    /**
     * next = oNext
     **/
    next()                   <--- 8 // 这里执行的是 oNext, 所以打印出的是 oNext called
    console.log('end fn4')   <--- 9
}

fn4 return 后,则会回到其调用点:6, 即回到 fn3 的函数栈, 继续执行 fn3,然后 fn3 return,依次类推:

function fn1(next){
    console.log('fn1')       <--- 1
    /**
     * next = () => fn2(() =>fn3(() => fn4(oNext)))
     **/
    next()                   <--- 2
    console.log('end fn1')   <--- 12
}

function fn2(next) {
    console.log('fn2')       <--- 3
    /**
     * next = () =>fn3(() => fn4(oNext))
     **/
    next()                   <--- 4
    console.log('end fn2')   <--- 11
}

function fn3(next){
    console.log('fn3')       <--- 5
    /**
     * next = () => fn4(oNext)
     **/
    next()                   <--- 6
    console.log('end fn3')   <--- 10
}

那么怎么实现 composed 的自动生成呢?

How?

compose([fn1, fn2, fn3, fn4])

-->

let composed = (oNext) => fn1(() => fn2(() =>fn3(() => fn4(oNext))))

和之前那的 compose 类似:

/**
 *
 * @param funs 函数列表,例:[fn1, fn2, fn3, fn4]
 * @returns composed 函数,例:(oNext) => fn1(() => fn2(() =>fn3(() => fn4(oNext))))
 */
function compose(funs) {
  return (oNext) => {
    let length = funs.length

    let ret = oNext

    // 从后往前构造参数
    // () => fn1(() => fn2(() =>fn3(() => fn4(oNext))))
    for (let i=length-1; i>=0; i--) {
      let fn = funs[i]
      // ret 是引用,我们先存一份
      let oldRet = ret
      ret = () => fn(oldRet)
    }

    // fn1(() => fn2(() =>fn3(() => fn4(oNext))))
    return ret()
  }
}

let composed = compose([fn1, fn2, fn3, fn4])
composed(oNext)

显然,我们得到的 composed 为:

(oNext) => {
  let length = funs.length

  let ret = oNext

  // 从后往前构造参数
  // () => fn1(() => fn2(() =>fn3(() => fn4(oNext))))
  for (let i=length-1; i>=0; i--) {
    let fn = funs[i]
    let oldRet = ret
    ret = () => fn(oldRet)
  }

  // fn1(() => fn2(() =>fn3(() => fn4(oNext))))
  return ret()
}

等价于: let composed = (oNext) => fn1(() => fn2(() =>fn3(() => fn4(oNext))))

同样的,我们将 compose 用 es6 语法简写:

function compose(funs) {
  return oNext => funs.reduceRight(
    (previous, current) => () => current(previous), oNext
  )()
}

完整源码: https://gist.github.com/njleonzhang/72077c1a1f6704e175c2d4ead81fdb3a

结语

本文一步一步地介绍了 compose 函数的一种实现,希望有助于读者理解。