关于源码的分析有很多。但大多是一上来直接贴出代码,然后一行一行加注释。
当然这样也没啥毛病,函数简单的还好,如果函数复杂,你就在短时间内很难在脑海中反推出各种输入输出情况,以至于看到后面想睡觉。
当我们阅读一个已经沉淀了好几年的,并且拥有大量开发实践的库时,透过作者的测试用例看源码会是一个比较好的方式。
这样,它立马就会让你知道某个函数干了啥,各种输入输出用例是怎样的。
优秀的库,测试代码一般都比功能代码多。
在我看来目前redux中间件就是函数式编程中的包菜式组合a(b(c(d(...args))))
,其实就是KOA中的经典的洋葱圈模型。
为什么会有中间件这么一说,在我看来就是由于业务灵活多变,导致原有的函数不能满足业务的需求,需要增强。
当然这个增强并不是直接修改原有的函数代码(这样就太low了,而且也不易维护和扩展)。
而是通过函数式编程(functional programming)中的提倡的高阶函数来增强的。
在开始“聊”之前,我们先看看常见的函数式编程模型几个概念(redux这个库就是函数式编程的典范):
- 闭包(Closure)>>>保留局部变量不会被释放的代码块,称之为闭包。
- 高阶函数(Higher-order function) >>>接受或返回一个函数的函数称之为高阶函数。
- 柯里化(Currying)>>>给一个函数的传递部分参数,返回一个接受其他参数的新函数。
- 组合(Composing)>>>将多个函数的能力合并,返回一个新的函数。
言归正传,既然是由浅入深的“聊”,那么就由一个实际工作中的问题来入手。
工作中我们常看到这样的代码:
function success(state) {
if(state.code == 200) {
//bulalalla一堆处理逻辑
return;
}
if(state.code == 411) {
//bulalalla一堆处理逻辑
return;
}
// 然后某一天,因为业务需求,又要针对412状态码的错误提示进行特殊指引和提示
// 然后某一天,因为业务需求,又要针对413状态码的错误提示进行特殊指引和提示
// bulalala……这样一直修改success函数本身式的增强,如果一个不小心手一抖,改错了,嘿嘿~~
// 在这里,我们优雅的方式是,我们希望状态码的处理方式可插拔的,随意组合等。
// 中间件技术能帮我们搞定最这一切
throw Error(state.code);
}
$.getJson('xx.com/api', success);复制代码
接下来我们用高阶函数来增强它,而不是修改其本身。
这些个高阶函数就是我们中间件的雏形。
function success(data) {
return data; // 不做任何code处理
}
// code等于200的处理的高阶函数(接收一个函数作为参数,并返回一个新的函数)
function code200(func) {
return function(...args) {
if(data.code === 200){
//.....code等于200的处理
} else {
func(...args);
}
}
}
// code200(success) --->> 瞬间就让success具有了处理状态码200的能力
$.getJson('xx.com/api', code200(success));
// 再增加一个code等于411的处理
function code411(func) {
return function(...args) {
if(data.code === 411){
//.....code等于411的处理
} else {
func(...args);
}
}
}
// code411(code200(success)) --->> 瞬间就让success函数具有了处理状态码411, 200的能力
$.getJson('xx.com/api', code411(code200(success)));复制代码
有些同学可能不太懂这个code411(code200(success))
是个什么鬼?
//这里我整理一下
code411(code200(success))
//就等于===
function code411(...args) {
if(data.code === 411){
//.....code等于411的处理
} else {
code200(...args); // 通过高阶函数缓存的函数引用func -> code200
}
}
function code200(...args) {
if(data.code === 200){
//.....code等于200的处理
} else {
success(...args); // 通过高阶函数缓存的函数引用func -> success
}
}
function success(data) {
return data; // 不做任何处理
}
复制代码
到此为止,就形成了一个洋葱圈模型的middlewares的链条。
每次这样去写a(b(c(d(e(origin)))))
这样的玩意儿,实在是恶心,所以我们应该用一个函数来帮我们做这件事。
OK,我觉得我现在可以贴上Redux的compose API的源码和测试用例来分析了^_^……
大家看到没,compose功能代码才10行,而它的测试用例代码居然有整整52行。
透过测试用例,我们清晰的知道compose代码输入和输出,那么研究起源码来就轻松多了(至少在心理上?)。
在分析这段代码之前,我先看看MDN对reduce函数的解释。
是不是,瞬间秒懂。
接下来我们就来分析下componse API中的reduce是如何工作的。
Redux compose 函数源码
export default 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)))
}复制代码
Redux compose 函数 'composes functions from right to left' 测试用例代码
it('composes functions from right to left', () => {
const a = next => x => next(x + 'a')
const b = next => x => next(x + 'b')
const c = next => x => next(x + 'c')
const final = x => x
expect(compose(a, b, c)(final)('')).toBe('abc')
expect(compose(b, c, a)(final)('')).toBe('bca')
expect(compose(c, a, b)(final)('')).toBe('cab')
})复制代码
透过上面代码,我们清晰的看到了输入与输出。
调用compose(a, b, c)
会执行funcs.reduce((a, b) => (...args) => a(b(...args)))
。
然后,会返回一个创建中间件链的函数(...args) => a(b(c(...args)))
。
在这里,为什么会返回(...args) => a(b(c(...args)))
,我们详细分析下:
- funcs.reduce((a, b) => (...args) => a(b(...args)))
-
prev1 = (...args) => a(b(...args))
res = (...args) => prev1(c(...args))
c(...args)
看成一个参数传递给 prev1
,会变成 a(b(c(...args)))
。
那么最终res 结果就是(...args) => a(b(c(...args)))
。
接下我们执行res(final)
创建创建中间件链。
其实就是执行a(b(c(final)))
,这里的final
就是我们要增强的函数。a(b(c(final)))
执行流程就是上面的code411(code200(success))
(已做分析)。
所以a(b(c(final)))
返回一个被增强了的final
函数,我们在这里定义它为final2
。
所以最后函数执行应该是这个样子的:
final2('')
next('' + 'a')
next('a' + 'b')
next('ab' + 'c')
'abc'
以上Redux洋葱圈模型中间件技术核心(按传入的顺序,把各个中间件函数链接起来,然后顺序执行来增强原有函数的处理能力)。
掌握好它,在项目中扩展业务处理函数将会变得非常容易和清晰可维护。
理解了compose
函数后,我们来看看Redux提供的applyMiddleware
函数就会变得简单很多。
同样,我们可以透过测试用例看源码。
这是一段典型的函数式编程代码。它是闭包,高阶函数,柯里化,组合等等这些常见的函数式编程模型的综合运用。
我在前面说过,中间件函数你可以理解为其实就是在不改变原来函数代码的情况下,增强原来函数的处理能力。
透过let dispatch = store.dispatch
和dispatch = compose(...chain)(store.dispatch)
我们清晰的看到函数并没有直接给里面的store.dispatch
赋值,而是通过compose
组合[中间件函数]和[它本身],来返回一个增强版的dispatch
函数。函数最后返回给用户一个全新的store
单例(return {...store,dispatch}
)。
所以,我们用户执行store.dispatch
就会经过一堆函数进行逻辑处理。
附上张中间件执行图:
最后,祝大家生活愉快!