上一篇文章回答了前两个问题,而这一篇则回答最后一个问题。

Redux如何使用?

前一篇文章描述了Redux的基本要素,并且梳理了一下Redux的数据流动图

Alt text

首先,我们的View上体现出的任何操作或者事件都会调用Dispatch方法触发Action
Action 会告知了type和data
然后,Store自动调用Reducer,并且传入两个参数:当前 State和收到的Action。 Reducer会返回新的State 。并且这个过程是纯函数式的,返回的结果由入参决定,不会随着外界的变化而改变。

知道概念了,那么我们从代码层面如何搭建出Redux数据流的架构呢?

最基本的Redux数据流

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
import { createStore } from 'redux';
const initialState = {
items: []
};
const reducer = (state = initialState, action = {}) => {
const { type } = action;
switch (type) {
case "ADD_ITEM":
return Object.assign({}, state, {
items: state.items.concat(action.item)
});
break;
case "GET_ITEM_LIST":
return Object.assign({}, state, {
items: action.items
});
break;
}
}
const store = createStore(reducer)
store.subscribe(() => {
console.log(store.getState())
})
store.dispatch({type: 'GET_ITEM_LIST','items':[{id:1,name:'test'}]})
store.dispatch({type: 'ADD_ITEM','item':[{id:2,name:'test2'}]})

这个代码段是最简单的Redux数据流例子,把创建Store、ActionCreator、Reducer全放在了一个文件中,在实际项目中,这当然不可取。
其实每个Redux的实体概念都可以拆解成一个单独的文件去管理。
在项目中为了项目的规范化,我们可以对所有的action_type常量进行统一的管理,放到单独的文件中

1
2
3
4
const GET_ITEM_LIST = 'GET_ITEM_LIST'
const ADD_ITEM = 'ADD_ITEM'

当然,也许最需要进行拆分是Reducer,在有些规模的实际项目中,state往往比较庞大。对于Reducer而言,我们最好根据相应的业务需求拆分出不同Reducer来管理。

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
import { createStore,combineReducers } from 'redux';
const itemReducer = (state = {}, action = {}) => {
const { type } = action;
switch (type) {
case "ADD_ITEM":
return {
...state,
items: state.items.concat(action.item)
}
break;
case "DELETE_ITEM":
return {
...state,
items: state.items.filter((item)=> {
return !(item.id === action.itemId);
})
}
break;
}
}
}
const ListReducer = (state = {items: []}, action) => {
const { type } = action;
switch (type) {
case "GET_ITEM_LIST":
return {
...state,
items: action.items
}
break;
case "CLEAR_ITEM_LIST":
return {
...state,
items: []
}
break;
}
}
}
const reducer = combineReducers({itemReducer,ListReducer})

可以看到,Redux其实本身也提供了combineReducers方法来帮助开发者合并Reducer,可以让每个Reducer互相独立。

Middleware中间件

Redux中,一切数据都是从一个状态到另一个状态,那么也许我们需要在这个状态间添加一些自己的方法或者功能呢?
这个时候就需要Middleware,Middleware发生在发送Action时,也就是调用store.dispatch()。
在Middleware中,我们通过调用next(action)函数就可以把Action传递给Reducer。
例如一个典型的简易版redux-logger模块,

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
import { createStore, applyMiddleware } from 'redux';
const initialState = {
items: []
};
const reducer = (state = initialState, action = {}) => {
const { type } = action;
switch (type) {
case "ADD_ITEM":
return {
...state,
items: state.items.concat(action.item)
}
break;
case "GET_ITEM_LIST":
return {
...state,
items: action.items
}
break;
}
}
const reduxlog = store => next => action => {
console.log(`prev state`, store.getState())
console.log(`action:`, action.type)
next(action)
console.log(`next state`,store.getState())
}
const store = createStore(reducer, initialState, applyMiddleware(reduxlog))
store.subscribe(() => {
console.log(store.getState())
})
store.dispatch({type: 'GET_ITEM_LIST','items':[{id:1,name:'test'}]})
store.dispatch({type: 'ADD_ITEM','item':[{id:2,name:'test2'}]})

Alt text

中间件处理了改变前和改变后的状态,写法也非常容易理解,如果有业务需要对状态集中处理,通过中间件的方式也不失为一种选择。
文章头部的那张图,如果加上中间件,就是这样:

Alt text

中间件的顺序

1
2
3
4
5
6
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);

中间件的调用顺序其实还是有一定讲究,这就要从Middleware本身的设计思想来说明。
Redux深受函数式编程的影响,中间件的设计也不例外,

middleware 的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的柯里化 curry,一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个 middleware 进行层层调用,动态地对 store 和 next 参数赋值。

Redux的applyMiddleware会将所有的中间件组合串联,

compose 将 chain 中的所有匿名函数,[f1, f2, … , fx, …, fn],组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, … , fx, …, fn],从左到右依次执行( 所以顺序很重要)

具体中间件的实现思路不详细展开,知乎上有一篇专栏分析的很到位,有兴趣可以看一下 链接

上面的例子里如果logger中间件不放置在最后面,输出结果会不正确。

异步问题

刚刚我们看了那么多示例代码?但是至此,Redux一直没有解决异步的问题。试想,如果我在页面输入一段内容,然后触发了一个动作,此时需要向服务端请求数据并将返回的数据展示出来。这是一个很常见的需求,但是涉及到异步请求,刚刚的示例中的方法已经不再适用了。那么Redux是如何解决异步问题的呢?

没错,还是Middleware,Middleware只关注dispatch函数的传递,至于在传递的过程中干了什么中间件并不关心。
这里不得不提redux-thunk这个中间件

redux-thunk的基本思想就是通过函数来封装异步请求,也就是说在actionCreater中返回一个函数,在这个函数中进行异步调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
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的源码看一下,讲真代码量就十几行,那么这里做了什么处理呢?
其实关键的就一句代码

1
2
3
4
5
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

我们知道Action正常其实返回的是一个js对象,如果没有经过Middleware的处理,是不符合Redux逻辑,会抛出异常的,所以redux-thunk只做了一件事件那就是让Action能够兼容 return 函数,redux-thunk内部再去消化掉这个函数。

1
2
3
4
5
6
7
8
9
function actionCreate() {
return function (dispatch, getState) {
api.fetch(data).then(function (json) {
dispatch(json);
})
}
}

总结

以上是最近一段时间通过在业务不断实践,然后学习和思考的总结。

在这期间发现,很多东西需要事先去积累,要拓宽自己的视野,如果自己不知道某种设计思路并且没有相关经验时,很难想到点上。

技术与业务是相辅相成的,技术能够很好的帮助自己拓宽视野,能设计更好的项目架构;而业务能够让技术得以实践,并且发现技术上可能还存在的问题,从而积累经验。

这个思路我通过这段时间的学习感悟到的,今后的学习也会沿着这个思路走下去。