Redux 中间件机制探底

状态管理方案之前仅仅接触过 Vuex, 使用 React 开发时,难免要调研一下 React 技术栈下的状态管理方案,发现有 Redux 和 Mobx 相关流派。以下内容仅针对 Redux 展开讨论。

在使用 Redux 的过程中发现,有这么几个知识点还是比较容易接受:

  1. 对状态的修改必须 dispatch 一个 action, 保证状态的修改可控易管理
  2. reducer 必须是一个纯函数,不能对 state 直接进行修改,而是每次返回一个全新的 state。纯函数的实现可以提高运行效率,固定的输入产生固定的输出
  3. redux 本身有一个「订阅」的概念,状态更改后, Redux 会将依次执行订阅者,在订阅者的事件回调函数中可以通过 store.getState() 拿到最新的状态

此时有这样一个疑问: 上面仅仅讨论了一个同步的情况,对于一些异步以及存在其他副作用的 action 产生过程如何处理,带着这个疑问,看了官方文档以及一些 Demo 实现。这个过程出现了 redux-thunk、redux-promise、 redux-saga 等处理方案。这些又是做什么的,分别都解决了什么问题?

这就要讨论一下 Redux 的中间件机制,在 Redux 中有这样一个 API, applyMiddleware, 主要用于注册 Redux 中的中间件。

阅读文档的过程中,主要搞清楚了一个最基本的世界观问题: Redux 的中间件是用来做什么的?它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 也就是以上讨论的 redux-thunk、redux-promise、redux-saga 等都是一个个的 Redux 的中间件,使用时需要在 Redux createStore 时注册,他们分别增强了 Redux dispatch 的能力。同样可以理解为:在应用这些中间件后,使用的 dispatch 已经不是 Redux 原本的 dispatch,都是经这些中间件改写后的 dispatch。这样我们就能再真正产生 action 之前做一些副作用的封装。

以 redux-thunk 为例,我们可以清晰的的看清楚这个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

以上就是 redux-thunk 这个库的源码部分,「短小精悍」这个词来形容这个库一点都不过分了。究其实现,可以发现,redux-thunk 这个中间件主要是提供了 dispatch 一个 function 的能力。正常来说 Redux 的 dispatch 仅仅能 dispatch 一个纯js对象,也就是 action。 使用 redux-thunk 后,我们可以接收一个 function, 这个 function 会被得到调用,并被传入 dispatch 这个参数,真正的 dispatch 发生在这个 function 内部。

至于 Redux 的中间件机制是如何实现的,在看了其源码实现后,更是巧妙。

首先我们要明确一下 「中间件」 这个概念。个人粗俗理解:中间件就是一个「管道」,只要你过了这个「管道」,都会被这个「管道」接管,「管道」不会拦住不放,而是将你「蹂躏」一番再放了你,当然也有可能不「蹂躏」你,顶多查一下户口(传说中的日志中间件)。凡是经「蹂躏」过的不管是从精神上、还是肉体上都不再是原来的你我,可能是一蹶不振,也可能是奋发图强。

有了以上的理解,当我们在看这个事情的时候就好理解了。可能会有多个中间件,只要进了这个「屋」,就要依次经历这些中间件。

1
2
3
4
5
6
const reduxMiddleware = ({dispatch, getState}) => (next) => (action) => {
// 做一些查户口以及蹂躏相关的事情

// 放行
return next(action);
}

这是一个 redux middleware 的通用实现。当我们在 applyMiddleware 时发生了什么?

1
2
3
4
5
6
7
8
9
10
11
const chain = middlewares.map(middleware => middleware({
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}));

const dispatch = compose(chain)(store.dispatch);
return {
...store,
dispatch
}

大致意思就是将所有的 middleware 传入,并通过 compose 这个函数将所有的中间件组合并返回一个 dispatch 函数, 此时的 dispatch 不是 redux 原本的 dispatch 实现,而是一个经中间件增强了的 dispatch,这里面有一个控制权的反转,即将原本的 dispatch 功能作为参数传入,在函数内部完成 dispatch 的逻辑。此时的 dispatch 可能是这样的:

1
2
3
4
5
6
7
8

const dispatch = (dispatch) => {
// do things
// 这里经过了所有注册过的中间件的处理
// do things
return action => dispatch(action);
}

Redux 中间件实现的关键是 compose 函数,compose 函数利用 Array.prototype.reduce() API,完成所有中间件函数的依次调用,并返回如上所示的一个函数。

1
2
3
4
5
6
7
8
9
function compose(funcs) {
return function (dispatch) {
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a,b) => (...args) => a((b(...args))));
}
}

如上便是整个中间件机制的实现过程。因为中间涉及到一些函数柯里化的内容,有些函数嵌套较深才能返回,如果感觉到晦涩,可以看这个简洁版的代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 这是一个中间件
const a = (next) => (action) => {
console.log('经过了 a 中间件的蹂躏');
return next(action);
};

// 这是一个中间件
const b = (next) => (action) => {
console.log('经过了 b 中间件的蹂躏');
return next(action);
};

// 这是一个中间件
const c = (next) => (action) => {
console.log('经过了 c 中间件的蹂躏');;
return next(action);
};

// 原版的 dispatch
var rawDispatch = (action) => {
console.log('终于轮到原生的dispatch action了,派发了: ', action);
return action;
}

/** 以下是 applyMiddlware 的实现原理, 开始注册中间件 */
var arr = [a, b, c];

var res = arr.reduce((a, b) => (...args) => a(b(...args)));

var enhanceDispatch = res(rawDispatch);


// 调用一个增强的dispatch,会发现中间件逻辑会一次处理
enhanceDispatch('add');

// 经过了 a 中间件的蹂躏
// 经过了 b 中间件的蹂躏
// 经过了 c 中间件的蹂躏
// 终于轮到原生的 dispatch action了,派发了: add
// "add"

参考链接:

  1. https://www.redux.org.cn/docs/advanced/Middleware.html
作者

monster1935

发布于

2019-12-27

更新于

2024-09-25

许可协议