背景
react火了有一年多了,项目里面一直没去用,主要是有我自己的考虑:1.公司web前端就一个人,而react从上手难度,协同合作来看更适合大的团队,小的团队像我这样的,使用vue更加容易上手和掌控。2.react+redux+react-router+redux-dev-tool+webpack+immutablejs全家桶学习曲线算是很高。最近项目不多,时间空闲就酝酿着对之前的一个项目用react+redux重构一次。这里先讲一下redux的源码。
关于redux的api使用推荐看一下阮老师的三篇文章,讲的很明白,我就不多说了
思路
学习新技术之前我们应该首先找到最规范的boilerplate,这个最规范的自然就是源码中的examples了,这里我以redux源码中的real-world为例(因为这个example最复杂综合); redux源代码很少,所以没看之前还是很有信心的,在src/index.js中redux暴露出如下api:
1 2 3 4 5 6 7 8
| export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose }
|
先从createStore看起,createStore的调用被封装store/configureStore.js文件下,这里又区分了configureStore.dev.js和configure.prod.js文件。主要区别就是redux-dev-tool加入和reducer模块hotload两个功能的区别。考虑到dev功能更加齐全,就以dev的代码为例:
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
| import { createStore, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' import createLogger from 'redux-logger' import api from '../middleware/api' import rootReducer from '../reducers' import DevTools from '../containers/DevTools' export default function configureStore(preloadedState) { const store = createStore( rootReducer, preloadedState, compose( applyMiddleware(thunk, api, createLogger()), DevTools.instrument() ) ) if (module.hot) { module.hot.accept('../reducers', () => { const nextRootReducer = require('../reducers').default store.replaceReducer(nextRootReducer) }) } return store }
|
createStore核心代码如下
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| export default function createStore(reducer, preloadedState, enhancer) { if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } function dispatch(action) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } var listeners = currentListeners = nextListeners for (var i = 0; i < listeners.length; i++) { listeners[i]() } return action } dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }
|
enhancer不为空就会执行enhancer函数并返回它的值;如果为空最终会返回一个默认值包含了dispatch,subscibe等api等等。
这里enhancer正好对应
compose(
applyMiddleware(thunk, api, createLogger()),
DevTools.instrument()
)
下一步就看applyMiddleware了,这个api是添加中间件。比如redux-thunk中间件就是为了解决异步action的问题而产生的,比如ajax请求(为什么要异步要返回函数这个阮老师的博客讲过了):
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
| function receivePosts(postTitle, json){ return { type: FETCH_SUCCESS, title: postTitle, data: json } } dispatch(receivePosts('加载成功', json)) export function indexGetBanner(status){ return (dispath, getState) => { return fetch(HOST+'SAAS_H5_ListBanner', { method: 'POST', headers: { 'sid': 'AA8yXxJNfRiWtKy4omdwqCuxyZpSsh2a4vdUkFC5/ww=' } }).then(response => response.json()) .then(json => { dispatch({ type: GET_BANNER, bannerList: json.body.bannerList }) }) } } dispatch(indexGetBanner())
|
而在createStore中dispatch的源码我们也看到,一开始使用isPlainObject进行了检验,所以这里为了使dispatch能够接受函数作为参数。我们就要使用到中间件。对dispatch进行改造和包装。
applyMiddleware源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer) var dispatch = store.dispatch var chain = [] var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
|
redux-thunk和demo中自定义的api中间件源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function thunkMiddleware({ dispatch, getState }) { return next => action => typeof action === 'function' ? action(dispatch, getState) : next(action); } module.exports = thunkMiddleware export default store => next => action => { }
|
可以发现所有的中间件都是三层函数。在applyMiddleware中,chain = middlewares.map(middleware => middleware(middlewareAPI))这段代码执行了最外层的函数。middlewareAPI中的getState,dispatch参数也和thunkMiddleware函数的参数相契合。
这个时候的chain实际上是类似于这样的函数数组
1 2 3 4 5 6 7 8
| [ next => action => { }, next => action => { } ]
|
再看下一行compose函数的执行。
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } const last = funcs[funcs.length - 1] const rest = funcs.slice(0, -1) return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args)) }
|
reduceRight和reduce的区别在于运算的操作是从右侧开始的,
1 2 3 4 5 6 7 8 9
| [1,2,3].reduce(function(pre, cur, index, arr){ console.log(pre+'----------'+cur); return pre+cur;}, 10) 10----------1 11----------2 13----------3 [1,2,3].reduceRight(function(pre, cur, index, arr){ console.log(pre+'----------'+cur); return pre+cur;}, 10) 10----------3 17----------2 21----------1
|
compose方法return出来的就放方法串行执行: 比如入参funcs是[func1, func2, func3, func4]
最终返回值是:
1 2 3
| function(...args){ return func1(func2(func3(func4(...args)))) }
|
对应上面chain的结构,最终的返回值也就是
1 2 3 4 5 6 7 8 9 10 11 12 13
| function(...args){ var r1 = (function f4(next){ return function f4action(action){ } })(args); var r2 = (function f3(next){ return function f3action(action){ } })(r1); }
|
这个方法返回之后立即被执行了,参数就是store.dispatch。这是原来的没有被改变过的dispatch;想一下r1实际就是返回 参数为action的匿名方法。这个函数唯一被改变的就是其中的next已经被替换为store.dispatch。这个函数又作为下一个中间件的next,替换掉下一个匿名action函数中的next函数,所以最终新生成的dispatch函数应该这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function f1action(action){ (f2action(action){ (f3action(action){ store.dispatch(); })() })(...params) }
|
可以十分清楚的看到,新的dispatch添加入了每个中间件的逻辑。这就是中间件的原理。写完这些之后我看了下关于middleware的介绍,确实是这个思路;强烈建议看下这个文章,正向思考,一步一步解释了为什么applyMiddleware代码会是这种样子。
所以异步action最后调用dispatch,参数是一个function。而这个function作为action一直被传递到最里层。一般第一个middleware是thunk,由于使用reduceRight,它正好位于最里层。action =>
typeof action === ‘function’ ? action(dispatch, getState) : next(action); 这样异步action返回的方法正好被执行了,dispatch,getState参数也正好对上了。
中间件的分析写到这里了,看完之后真的觉得这种链式执行很巧妙,redux还有一个api bindActionCreators留在下一篇react-redux讲解,这个与其中的connectapi有关