常见的函数组合

1,215 阅读2分钟

函数组合在我们的实际开发中经常会碰到。我们来看看他们的实现方式。

普普通通的 compose

普普通通的函数组合长这样。返回了一个方法,方法里 a 接收了 b 的执行结果,然后返回 a 的执行结果。

function compose (a, b) {
    return function (...args) {
        return a(b(...args))
    }
}

看个简单的用法

const addition = num => num + 1

const multiply = num => num * num

使用 compose ,就看的更加简洁了些。

const composeFunc = compose(multiply, addition)

composeFunc(2)

但是目前的 compose 方法并不能满足两个方法以上的组合,我们看看常用redux 和 koa 的实现。

redux compose

这是 redux 的实现写法

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)))
}

第一眼看,可能有些难以理解。

const composeFunc = compose(a, b, c, d)

我们换一个手动的写法就相当于

const composeFunc = (...args) => a(b(c(d(...args))))

他的入参从右往左执行,最右边的 d 方法接受 args 参数,之后的每一个接受前一个返回结果作为参数。直到 a 执行返回结果。

在 redux 中,主要用于组合多个 store 增强器,依次执行。

所有 compose 工作就是让你编写深度嵌套的函数时,避免了向右偏移(使用深度右括号)。

我们思考一个问题,如何实现一个更符合直觉的从左往右执行的组合函数呢?

koa-compose

这是 koa 的实现写法。也是洋葱模型的核心。

function compose (middleware) {
    // 规定 middleware 必须是数组,且都是 function
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }

    return function (context, next) {
        let index = -1

        // dispatch 从 middleware 的第一个方法开始执行
        return dispatch(0)

        function dispatch(i) {
            // 边界处理
            if (i <= index) {
                return Promise.reject(new Error('next() called multiple times'))
            }
            index = i
            let fn = middleware[i]
			
            // 到达洋葱模型的中心位置
            if (i === middleware.length) {
                fn = next
            }
            // 边界处理
            if (!fn) {
                return Promise.resolve()
            }

            try {
                // 将 context 和 下一个中间件方法(next)传入到当前的中间件方法。
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

这里插一句,我们在写 koa 时,会调用 app.use(...) 实际上就是往 middlewarepush 方法。

我们看一看具体用法。

async function foo (context, next) {
    context.number += 1
    console.log(`step 1 : ${context.number}`)
    await next()
    context.number += 1
    console.log(`step 4 : ${context.number}`)
}
async function bar (context, next) {
    context.number += 1
    console.log(`step 2 : ${context.number}`)
    await next()
    context.number += 1
    console.log(`step 3 : ${context.number}`)
}

使用 compose 组合上面的方法。

const composeFun = compose([foo, bar])

composeFun(
    { number: 0 },
    (context) => {
        console.log(`center : ${context.number}`)
    }
)

输出:

step 1 : 1
step 2 : 2
center : 2
step 3 : 3
step 4 : 4

在日常开发中,这种方式可以用在多个接口互相依赖等场景,用这样的方式去写,可以解耦代码逻辑,更加清晰。