在编程的路上,我们都是是先有了设计思路,而后才是选择使用哪种技术手段&编程模式。那么就洋葱模型而言, 在JAVA世界里面我们可以采用责任链的方式来实现。那JS的世界里呢,借助FP(函数式编程)该用什么方式来实现呢?
书接上文,我们推导出了compose函数。那么,问题来了compose函数和我们的洋葱模型有什么关系?
我们这里的洋葱模型是本质上是一层层的处理逻辑,而在FP世界里我们当然就是用函数来做处理单元,下文中的洋葱皮在redux中指代的就是一个个的middware,在KOA框架中同样也是采用这样的框架。实际上,在JAVA世界里的拦截器也是类似的实现。那么我们来思考两个问题:
洋葱皮的上下文是什么?
答:上下文是 dispatch和getState,为什么?
getState:这样每一层洋葱都可以获取到当前的状态。
dispatch:为了可以将操作传递给下一个洋葱
洋葱皮之前的顺序与关系?
答:从外往内逐层调用,某些情况下我们要从从第一层从新调用。比如:当你在这层action改变了状态,或者,进行了某种校验,发现不符合业务逻辑需要跳转。最常见的就是异步请求,当你的action是一个异步请求的时候,这时,当结果回来的时候从逻辑的完整性来说,是需要从第一层触分发action的。因为,此时回来的数据对所有的middware来说,都是需要处理的消息。
综上所述,实际上我们讨论的dispatch其实是有两个的。
用于从头开始的dispatch,也就是最终的dispatch;
用于跳到下一层洋葱皮的dispatch。
为了更好的说明,我们把跳到下一层的叫做next,当然,还有一个就是每次要传递的action。
所以说,我们引出了要实现的4个要点!
getState、next、dispatch、action。
思考:为什么要将所有洋葱皮复合后就得到了一个dipatch函数?
答: 因为,dipatch是连接action与reducer的桥梁,而每一层洋葱皮就是为了在这个传递过程中做某一些处理?比如日志,比如缓存等等。
下面我们来思考如何实现
那好,这时我们知道这个可以通过compose得到的最终的dipatch,那么上文我们说到,compose的前提是一系列相同声明的函数。相同声明指的是入参&返回值一直。
比如:
function f1(name: string): string{ return 'hello' }
function f2(name: string): string{ return 'hello' }
PS:这里,用的是flow-type的语法,这是一种静态类型检查的语法。后续,我会写相关的文章来分享。
我们知道dispatch是用来分发action的,所以推导出第一个函数
dispatch = (action) =>{};
问题来了,我们说compose必须是一组结构相同的函数,合并成一个函数。那在这里,我们既要传递dispatch又要传递action。该怎么办?
这时就需要亮出函数式编程的核心工具:闭包&高阶函数。
所以基于高阶函数思想这里我们得到了一个三阶的函数。
midware1 = (dipatch, getState) => next => action => {};
midware2 = (dipatch, getState) => next => action => {};
midware3 = (dipatch, getState) => next => action => {};
此时,我们遇到了一个问题,我们怎么让每个middware持有最终的dispatch呢?
这时,我们就可以用第二个核心工具:闭包。
借用闭包变量的强绑定引用的方式来解决,什么意思?
当你在一个函数声明范围内,持有一个声明外部的变量,那么就形成了一个闭包。而所有闭包内对变量的修改,都将会影响其它人。无论这个变量是对象或者是基本类型。
PS:为什么提到无论是基本类型和对象,因入参如果是一个JS对象,你是可以在函数内部直接修改其属性的值。有时初学者容易搞混这两个概念。
具体的实现伪代码如下:
let dispatch = ()=>{};
[mid1,mid2,mid3].map(mid=>mid({ getState, dispatch(){ return dispatch }}));
这样内部的dispatch就和外部的进行了强绑定。
最后只要把
dispatch = 最终的dispatch方法即可。
到此,我们完成了第一步,将getState和dispatch传递进了函数。在FP编程中,这样的编程心智模型是很常用的一种写法。
此时我们得到什么?
mid1: next=>action=>{};
mid2: next=>action=>{};
mid3: next=>action=>{};
那么下来,我们就是要将这些个dispatch变成一个dispatch,怎么做?没错就是用我们的compose函数。
compose(mid1, mid2, mid3);
串起来的调用栈,怎么触发这个调用栈呢?传入redux实现的dispatch,即可。
finalDispatch = compose([mid1, mid2, mid3])(redux.dispatch)
这时候就可以通过finalDispatch,来在mid之间进行action传递。
finalDispatch(action)的调用顺序是什么呢?我们来一起分析一下!这也是实现的关键,我们的需求是mid1->mid2->mid3->redux.dispatch,这样的写法是否符合我们的设计呢?
这里要注意compose一阶函数,和compose二阶函数的不同的地方,也是我想了很久才明白的地方。上一篇文章中我们讲到,copmse(f1,f2,f3)对应的调用模型是f1(f2(f3())),也就是说调用顺序为 f3->f2->f1。考虑以下代码:
那考虑下二阶函数的compose是什么情况
分析代码,当我们传入redux.dispatch的时候,结果是:
mid3.next = redux.dispatch
mid2.next = mid3返回的函数
mid1.next = mid2返回的函数
注意,我们复合等到的是一个2阶的函数,当我们传入redux.dispatch时,实际上函数已被降为一阶函数。此时mid1生成的函数出处在最上面的调用栈上。这就是compose二阶函数的妙处。通过一个降阶,把函数的调用顺序倒了过来。又是闭包的功劳,通过闭包我们是的每一个middware中可以拿到外部传入的next函数,从而实现顺序控制。
这个函数调用栈如何关联,通过mid中去调用前者传入的next函数。
而每一层mid都先执行自己的部分,而后再交给下一层进行处理,当然也可以选择直接跳回到第一层。
到这里,我们可以知道finalDispatch(action)的运行顺序是
mid1 -> mid2 -> mid3 -> dispatch,这样是符合我们的设计本意的。
从上面的分析中,我们能收获的心智模型是什么?
我个人觉得收获的是一种FP思维。我们知道ES本身也可以借助原型链模拟出面向对象你也可以使用责任链模式来实现。
而redux选择了FP,在FP的世界里,先把每一步的操作设计成一个个的一阶的函数,而后将所有这些函数,按照实现需要按顺序,通过高阶嵌套,compose、柯理化、部分参数化等等方式来实现逻辑的抽象。所以,升阶&降阶是一种具有很强大表现力的编程方式。
这就是FP与OOP的区别,多年OOP职涯我们被教导是反复的进梳理对象关系&抽象逻辑,来考虑设计与编程,
而FP则是从操作(函数)单元的角度出发考虑。
当然,一开始可能会有些不适应,就我个人而言有一种从新学习编程的赶脚。
这里,在补充一点,其实我们在JAVA的世界里,也在函数编程,不过我们一直做得是一阶函数的编程。当然借助内部类、接口也可以模拟出闭包的特性,但终究有点不伦不类。当然随着JDK引入拉姆达表达式(Lambda Expressions) 我们也可以窥见FP吸引力。
最后贴出代码:
functioncompose(...funcs) {
if(funcs.length ===) {
returnarg => arg
}
if(funcs.length ===1) {
returnfuncs[]
}
returnfuncs.reduce((a,b) => (...args) => a(b(...args)))
}
compose((next) => {
console.info(next);
return1;
},(next) => {
console.info(next);
return2;
},(next) => {
console.info(next);
return3;
},)();
compose((next) => {
console.info(1);
returnaction => {
//mid1
next(action);
};
},(next) => {
console.info(2);
returnaction => {
//mid2
next(action);
};
},(next) => {
console.info(3);
returnaction => {
// mid3
next(action);
};
},)(action => { });
console.info('-------------------------------------');
constmid1= api => {
console.info('mid1 api');
returnnext => (action) => {
console.info('mid1');
if(typeofaction ==='function') {
returnaction(api.dispatch);
}
letresult = next(action);
console.info('mid1:',result);
returnresult;
}
};
constmid2= api => {
console.info('mid2 api');
returnnext => (action) => {
console.info('mid2');
letresult = next(action);
returnresult;
}
};
constmid3= api => {
console.info('mid3 api');
returnnext => (action) => {
console.info('mid3');
letresult = next(action);
returnresult;
}
};
letdispatch= () => {throw newError('null');};
constapi = {dispatch: (...arg) =>dispatch(...arg) };
constmiddle = [mid1,mid2,mid3].map(func => func(api));
dispatch=compose(...middle)((action) => {
console.info('core dispatch',action);
return'core-'+ action.type;
});
console.info('compose finised');
console.info('result',dispatch((dispatch) => {
returndispatch({type:'1000'})
}));
下一期,我将分享我对TJ大神的co模块的学习与理解。
领取专属 10元无门槛券
私享最新 技术干货