Redux中间件理解

本文将从 Redux 源码入手,一步一步讲解 Redux 的中间件实现机制,重点讲解 applyMiddleware 如何将中间件串联执行,最后,编写一个我们自己的 Redux 中间件。

这里采用 Redux 的官方示例 TodoMVC 作为 demo,我们会在此工程的基础上拓展和 debug,帮助我们更形象地理解。

demo工程运行

先将 TodoMVC 的源码 clone 至本地,执行 npm i 安装依赖包,执行 npm start 开始运行,一个经典的 TodoMVC 示例。有了demo工程后,我们来解读一下 Redux 的部分关键源码。

createStore

Redux 通过 createStore 来创建一个 store 对象,要理解 applyMiddleware 的原理,先从 createStore 入手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}
...

从这一部分的源码可以看到,createStore 的三个参数依次为:reducer,preloadedState 和 enhancer。如果传入的 enhancer 为函数,则 createStore 传入 enhancer 并返回。

1
return enhancer(createStore)(reducer, preloadedState)

回到我们的 demo 示例,并没有应用 Redux 中间件,我们来手动添加。

打开 todomvc/src/index.js,先执行 npm i redux-logger --save 安装 redux-logger 中间件用来打印 Redux 日志,然后应用在代码中。

既然存在 enhancer 时,createStore 返回的就是 enhancer(createStore)(reducer, preloadedState),那么我们可以这样改写。这里的 enhancer 就是 applyMiddleware。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'
import { render } from 'react-dom'
// 新增引入 applyMiddleware
import { createStore, applyMiddleware } from 'redux'
// 引入 redux-logger
import logger from 'redux-logger'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'
import 'todomvc-app-css/index.css'

const store = applyMiddleware(logger)(createStore)(reducer)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

那么我们不禁要问了,为什么采用了上面的代码就可以实现打印日志的中间件呢?我们来看一下 applyMiddleware 部分的源码。

applyMiddleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
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)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

上面的代码虽然精简,但是理解起来却不是那么容易,首先是用到了柯里化(currying),本文不讲解柯里化的知识,如不了解可以搜索了解一下函数式编程入门教程

applyMiddleware 的总体结构可以看成

1
2
3
4
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
}
}

对应到我们在 demo 中写的 applyMiddleware(logger)(createStore)(reducer),罗列一下形参与实参的对应关系,即

…middlewares => logger
createStore => 从 redux 引入的 createStore
…args => reducer

applyMiddleware 依然使用 createStore 创建了 store 对象并且返回,只是改写了 store 对象的 dispatch 方法。

我们重点来看被改写的 dispatch 与原生 dispatch 区别。

  1. 将 getState 和 dispatch 作为第一个参数传入中间件数组,获得执行完毕后的 chain 数组。
1
const chain = middlewares.map(middleware => middleware(middlewareAPI))
  1. 组合串联 middleware
1
dispatch = compose(...chain)(store.dispatch)

此处便组成了新的dispatch,compose 你可能多多少少听说过,来见识一下它的源码。

1
2
3
4
5
6
7
8
9
10
11
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)))
}

其实现原理自行脑补(手动滑稽),我们先理解它干了什么。

dispatch = compose(fn1, fn2, fn3)(store.dispatch)

可以转换为

dispatch = fn1(fn2(fn3(store.dispatch)))

于是,新的 dispatch 就这样产生了,每个中间件会被依次执行,执行过程类似于 Koa 的中间件执行的非常经典的洋葱模型。只有最后一个中间件会触发 Redux 原生的 dispatch,将这个 action 分发出去。

在最后一个中间件中调用原生的 dispatch。

那么,中间件函数到底是什么样的呢?

中间件 createLogger 实例

我们来看精简版的 createLogger 源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function createLogger({ getState }) {
return next => action => {
const console = window.console;
const prevState = getState();
const returnValue = next(action);
const nextState = getState();
const actionType = String(action.type);
const message = `action ${actionType}`;

console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
return returnValue;
};
}

回到 const chain = middlewares.map(middleware => middleware(middlewareAPI)) 这个步骤,对应地,createLogger(middlewareAPI)返回的结构是:

1
2
3
4
next => action => {
// createLogger主体部分
...
}

接下来,执行 dispatch = compose(...chain)(store.dispatch),对应地,dispatch = compose(next => action => {...})(store.dispatch),去掉难以理解的compose,即 dispatch = action => {...},最后,store 中原生的 dispatch 被替换成这个新的 dispatch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch = action => {
const console = window.console;
const prevState = getState();
// 这里的next就是原生的 store.dispatch,最后被触发
const returnValue = next(action);
const nextState = getState();
const actionType = String(action.type);
const message = `action ${actionType}`;

console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
return returnValue;
};

redux-thunk

普通的 action 都是一个对象,但是异步的 action 返回的却是一个 function,这就需要使用中间件 redux-thunk。我们可以按照中间件的套路,自行写一个 redux-thunk 的实现。

其固定模式为

1
2
3
function middleware({ dispatch, getState }) {
return next => action => {...}
}
1
2
3
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => typeof action === 'function' ? action(dispatch, getState) : next(action)
}

如果得到的 action 是个函数,就用 dispatch 和 getState 当作参数来调用它,否则就直接分派给 store,从而实现异步的 action。

将上面的中间件应用至我们的 demo 中

todomvc/src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'
import { render } from 'react-dom'
// 新增引入 applyMiddleware
import { createStore, applyMiddleware } from 'redux'
// 引入 redux-logger
import logger from 'redux-logger'
// 应用thunk
import thunk from './thunk'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'
import 'todomvc-app-css/index.css'

const store = applyMiddleware(thunk, logger)(createStore)(reducer)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

todomvc/src/constants/ActionTypes.js

1
2
3
...
export const FETCHING_DATA = 'FETCHING_DATA'
export const RECEIVE_USER_DATA = 'RECEIVE_USER_DATA'

todomvc/src/actions/index.js

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
...
// 省略原有代码
export function fetchingData(flag) {
return {
type: types.FETCHING_DATA,
isFetchingData: flag
};
}

export function receiveUserData(json) {
return {
type: types.RECEIVE_USER_DATA,
profile: json
}
}

export function fetchUserInfo(username) {
return function (dispatch) {
dispatch(fetchingData(true))
return fetch(`https://api.github.com/users/${username}`)
.then(response => {
console.log(response)
return response.json()
})

.then(json => {
console.log(json)
return json
})

.then((json) => {
dispatch(receiveUserData(json))
})

.then(() => dispatch(fetchingData(false)))
}
}

总结

写这篇文章之前,我对 Redux 中间件一直是一知半解的状态。在深入理解其实现原理后,有了一种恍然大悟的感觉,在今后的开发过程中也更加得心应手。

参考链接