背景

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) {
// Enable Webpack hot module replacement for reducers
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
}
//*****
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
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
//同步action
function receivePosts(postTitle, json){
return {
type: FETCH_SUCCESS,
title: postTitle,
data: json
}
}
//同步action激发方法,dispatch参数为对象
dispatch(receivePosts('加载成功', json))
//异步action
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
})
})
}
}
//异步action激发方法,dispatch参数为匿名函数 (dispatch, getState) => {...}
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
//api
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)))) //执行顺序就是f4,f3,f2,f1
}

对应上面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){
//f1action逻辑...
//next调用被替换为如下
(f2action(action){
//f2action逻辑...
//next调用被替换为如下
(f3action(action){
//f3action逻辑...
//最后一个中间件,next被替换为store.dispatch
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有关