Redux的Middleware中间件以及thunk源码解析

“ 很早之前就读了中间件相关的源码,最近终于找了一个时间来更新,今天涉及到的内容也是很精彩的,对中间件源码感兴趣的小伙伴也不要错过哟。”

在我工作项目中,涉及到了redux中间件的实现,在这里我就截取少部分代码给大家介绍源码解析,代码如下所示:

1
2
3
4
5
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
const createStoreWithMiddleware = compose(
applyMiddleware(thunk)
)(createStore);

1. 函数式编程

其实Redux的源码没有多少,但是在讲解之前我需要先给大家说一下什么是函数式编程?其实可以简单的理解为:复杂的问题可以细分为一个个的更小的函数,然后利用组合函数来进行解决。
在讲解完什么是函数式编程之后,当然接着就少不了什么是高阶函数?在js中一切皆对象,并且在js中函数是一等公民,那么使得可以接受函数作为参数,这种函数就称为高阶函数。

2. compose函数源码解析

compose函数是十分重要的,它的功能其实就是:从右到左,组合函数。场景使用有许多,比如在本文即将介绍的redux,还有就是我们经常使用到的webpack中loader执行。其实不要被compose这个函数给吓到了,它的实现核心其实是采用了reduce方法,然后将所有函数进行一个串联。reduce方法我就不过多介绍了,不懂的同学可以去看一下我之前写过关于reduce函数扩展的文章,或者去mdn文档中进行一个查看。
compose函数源码如下所示,有没有感觉到很简单,其实就是最后那个return语句中的reduce方法是精髓,如果你对reduce很熟悉的话,那么恭喜你对compose就理解了。当然你要是觉得reduce的使用是你的痛点的话,也不要慌,我这就一一道来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default function compose(...funcs) {
// funcs数组是空数组返回一个箭头函数
if (funcs.length === 0) {
return arg => arg
}
// funcs数组中只有一个值,返回该值
if (funcs.length === 1) {
return funcs[0]
}
// 其实最后返回的就是(...args) => a(b(...args)),至于reduce的执行重点看a(b(...args))
/**
* 我举一个例子:
* funcs=[fn1, fn2, fn3, fn4],reduce执行顺序:
* (1) a1 = (...args) => fn1(fn2(...args));
* (2) a2 = (...args) => a1(fn3(...args));
* (3) a3 = (...args) => a2(fn4(...args));
* 将(1)(2)带入到(3)得到:
* a3 = (...args) => fn1(fn2(fn3(fn4(...args))))
* /
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose函数执行顺序之后,返回值是另外一个函数;它的作用就是:从数组从后往前按照顺序执行,然后把前一个执行函数的返回值作为下一个执行函数的入参。关于这句话如果你觉得很绕的话,可以把它想象成洋葱圈模型,从内到外依次调用。下面我画的这幅图可以帮助你进行一个理解,图形如下所示:
洋葱模型执行由内到外
洋葱模型执行由内到外图

3. 柯里化函数

在讲到redux的中间件源码时,我们还需要了解一样常用的知识点,什么是柯里化函数?把接受多个参数的函数变化成接受一个单一参数的函数,返回接受余下的参数并且返回结果的新函数。可能有点绕,我来举个例子你就知道什么是柯里化了。
就举一个最常见的求和例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 非柯里化实现
function sum (a, b, c) {
return a + b + c;
}

// 柯里化实现
function sum (a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}

// 对上面柯里化函数进行一个简化(请记住这种写法,在中间件源码中会使用到)
const sum = a => b => c => a+b+c;

4. applyMiddleware源码解析

接下来我们就来揭开redux中间件神秘的面纱,其实中间件的源码很少,可能就20多行左右,其实中间件的核心是:改造dispatch方法,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
// 在这里dispatch方法还不能使用,因为还没改造成next方法
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 将中间件函数先遍历,执行里面的方法并返回,得到一个dispatch方法的中间件数组
const chain = middlewares.map(middleware => middleware(middlewareAPI))
/**
* 这里用到了我们在上面提及的compose方法,compose方法会将
* 包含了dispatch方法的中间件数组组合成一个嵌套的包装函数返回
*/
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}

是不是感觉其实也不是想象中那么难,把它简化以下就变成如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
// 其实applyMiddleware也是一个柯里化函数
function applyMiddleware(...middlewares) { // middlewares表示中间件d额数组
return function (createStore) { // createStore是用于创建store的函数
return function (...args) { // args其实是reducer
return {
...store, // createStore(reducer)
dispatch // 改造后的dispatch
}
}
}
}

关于这里为什么要传next?可以保证中间件不断裂。比如下面我们要介绍的redux-thunk中会有意外中断情况。
next函数会不断往下传递,如果你不传递next函数就会中断传递。因此当我们最后调用store.dispatch(store)时中断了dispatch的传递,因为我们只调用了store.dispatch方法并没有执行next向下传递,因此最后在这里结束了。
注意:如果我们要写自己的中间件的话,那么我们需要定义成如下所示的形式(柯里化函数):

const middleware = store => next => action => next(action);

5. redux-thunk源码

在介绍了上面中间件的源码之后,下面我们就来看一个常见的redux-thunk中间件,它的作用主要是:因为redux默认dispatch只能接受一个对象参数,而像函数或者promise它都是不允许的,此时就有了redux-thunk中间件的由来,它让dispatch可以是对象也可以是函数,这样就可以处理异步代码。接下来我们就来看看它的源码。

1
2
3
4
5
6
7
8
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}

有没有很眼熟,其实它的格式就是上面我们所要求的柯里化函数格式。其是redux-thunk源码很简单,就是判断传递的action是否是函数,是函数就执行action本身并将dispatch和getState传入,否则就继续执行next跳转到下一个中间件中。其实就是强化了dispatch让其可以执行函数的作用。
如果你对本章有了一定的了解话,可以来个三连哟。在后续公众号中我会来讲解react-redux中connect的源码,欢迎大家来看哟。