1、compose函数
用过redux
或者对Koa
、Express
等有点了解的同学应该都听过中间件这个名词,它可以让我们通过插件的形式对原本的代码执行流程进行安全的包装。其中最核心的思想就是组合函数compose
,如下:
// 代码摘自redux
const compose = function(...fns){
// 没有传入函数参数,就返回一个默认函数
if(fns.length == 0){
return (args) => args
}
// 只传入一个函数时,直接执行
if(fns.length == 1){
return fns[0]
}
// 组合函数
return fns.reduce((a,b) => (...args) => a(b(...args)))
}
不到10行的代码,却是整个中间件模式的精髓。可能有的同学会有这样的疑惑,上面的每行代码我都能看懂,可是这个函数到底是想表达个什么意思呢?
别着急,下面我来手摸手(限女生)带你去理解它。
2、compose实现原理
show me the code!!
var f1 = (arg1) => {
console.log(`fn1: ${arg1}`)
return 'hello1'
}
var f2 = (arg2) => {
console.log(`fn2: ${arg2}`)
return 'hello2'
}
var f3 = (arg3) => {
console.log(`fn3: ${arg3}`)
return 'hello3'
}
var f4 = (arg4) => {
console.log(`fn4: ${arg4}`)
return 'hello4'
}
const composed = compose(f4,f3,f2,f1)
composed('hello')
// 为方便分析,这里分开写的。等价于compose(f4,f3,f2,f1)('hello')
执行结果
接下来,让我们的大脑化身v8引擎,一步步的执行上码这段代码,看看会发生什么。
首先,我们快速的申明变量、赋值等操作。完成这些准备工作后,来到compose(f4,f3,f2,f1)
阶段。
compose(f4,f3,f2,f1)
执行过程解析
这个会返回什么呢?查看上面的compose函数实现,发现返回的是这行代码[f1, f2,f3,f4].reduce((a,b) => (...args) => a(b(...args)))
的执行结果。那这行代码具体进行了什么操作呢?可能很多同学跟我刚开始一样云里雾里,别着急,我们接下来就具体分析下:
reduce函数复习:
arr.reduce(callback,[initialValue])
reduce为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素。
callback
接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用reduce 的数组。
initialValue
作为第一次调用 callback 的第一个参数
- 第一次迭代:
a
指向f4
,b
指向f3- 返回一个函数(为方便行文,记做
A
)(...args) => f4(f3(...args))
- 第二次迭代
a
指向第一次迭代的返回值A
,b
指向f2
- 返回一个函数(记做
B
)(...args) => f4(f3(f2(...args)))
- 第三次迭代
a
指向第二次迭代的返回值B
,b
指向f1
- 返回一个函数(记做
C
)(...args) => f4(f3(f2(f1(...args))))
- 迭代完成,返回函数
C
给外部,并赋值给composed
composed('hello')
执行过程解析
紧接着,执行composed('hello')
, 即f4(f3(f2(f1('hello'))))
,这里又会发生什么事情呢?
- 首先
f4
入栈,f4
开始执行,执行时发现参数是一个函数的调用,那么就会执行该函数f3
,而不会直接执行f4
的函数体( console.log(fn4: ${arg4}
);return ‘hello4’)(f3,f2,f1同理) f3
入栈f2
入栈f1
入栈- 发现
f1
没有继续调用其他函数,开始执行f1
函数体,打印fn1: hello
;f1
完毕,出栈,返回'hello1'
给f2
- 开始执
f2
函数体,打印fn2: hello2
;f2
执行完毕,出栈,返回'hello2'
给f3
- 开始执
f3
函数体,打印fn3: hello3
;f3
执行完毕,出栈,返回'hello3'
给f4
- 开始执
f4
函数体,打印fn4: hello4
;f4
执行完毕,出栈,返回'hello4'
给最外层
(怎么样,是不是和回调地狱有点像 ๑乛◡乛๑)
3、中间件的实现
1.1 需求分析
上面的搞懂之后,接下来思考一下怎样实现这种形式,就是在进入下一个函数栈之前执行一些逻辑,然后在函数栈弹出后再执行一些逻辑。
我们先确定下实现的思路,要在f4执行的过程时候中间穿插着f3函数的执行,就是说f4要拥有对f3的控制权(上面的那个例子是进入f4之后直接执行了f3,没法控制它),那么怎么去实现把内层函数的执行控制权交给外面呢?答案就是:把内层函数包裹在一个新的函数里面,然后再返回就可以了。这样内层函数的执行权就层层向外的传递到了最外层函数。即 f4控制f3,f3控制f2…
// 我们质询要改动f1,f2,f3,f4
var f1 = (next) => {
return function(action){
console.log(`f1开始`)
const res = next(action + "_1")
console.log(`f1结束`)
return res
}
}
// f2、f3、f4 和上面类似
1.2 原理分析
我们先回到最开始的那个函数 f4(f3(f2(f1('hello')))
,可以看到f1
的返回值会成为f2
的参数(f2,f3,f4同理),这样我们可以画出下面这张图
// 上面代码等价于:
(next) => {
return function(action) {
console.log(`f4开始`)
const res = function(action) {
console.log(`f3开始`)
const res = function(action) {
console.log(`f2开始`)
const res = function(action) {
console.log(`f1开始`)
const res = next(action)
console.log(`f1结束`)
return res
}
console.log(`f2结束`) return res
}
console.log(`f3结束`) return res
}
console.log(`f4结束`) return res
}
}
这样就实现了上面的需求:f1
在f2
的逻辑里执行,f2
在f3
的逻辑里执行…。到此,一个简单的中间件就已经完成了。
注意,最后由于f4
返回的是一个函数,所以还得再调用一次:compose(f4,f3,f2,f1)(next)('action')
其中next
是传递给f1
的初始参数,字符串action
是传给f1
返回的函数的初始参数
1.3最终完整代码
var compose = function(...fns){
// 没有传入函数参数,就返回一个默认函数
if(fns.length == 0){
return (...args) => args
}
// 只传入一个函数时,直接执行
if(fns.length == 1){
return fns[0]
}
// 组合函数
return fns.reduce((a,b) => (...args) => a(b(...args)))
}
var f1 = (next) => {
return function(action){
console.log(`f1开始`)
const res = next(action + "_1")
console.log(`f1结束`)
return res
}
}
var f2 = (next) => {
return function(action){
console.log(`f2开始`)
const res = next(action + '_2')
console.log(`f2结束`)
return res
}
}
var f3 = (next) => {
return function(action){
console.log(`f3开始`)
const res = next(action + "_3")
console.log(`f3结束`)
return res
}
}
var f4 = (next) => {
return function(action){
console.log(`f4开始`)
const res = next(action + "_4")
console.log(`f4结束`)
return res
}
}
var reducer = (action) => {
return 'data from reducer' + ' ' + action
}
var next= (action) => {
console.log('开始dispatch')
return reducer(action)
}
compose(f4,f3,f2,f1)(next)('action')