compose
在前端被广泛使用,主要用于实现各种插件系统,如 koa
和 redux
的 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
}
现在思考一个问题,我希望用相同的参数逐一调用 fn1
到 fn4
的函数,但是我不想这样写:
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')
}
执行 fn1
的 next
参数,即 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 函数的一种实现,希望有助于读者理解。