Skip to main content

常见的函数组合

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

普普通通的 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

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