本文将从 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 区别。
将 getState 和 dispatch 作为第一个参数传入中间件数组,获得执行完毕后的 chain 数组。
1 const chain = middlewares.map (middleware = > middleware(middlewareAPI))
组合串联 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 => { ... }
接下来,执行 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(); 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 中间件一直是一知半解的状态。在深入理解其实现原理后,有了一种恍然大悟的感觉,在今后的开发过程中也更加得心应手。
参考链接