上一篇文章回答了前两个问题,而这一篇则回答最后一个问题。
Redux如何使用?
前一篇文章描述了Redux的基本要素,并且梳理了一下Redux的数据流动图
首先,我们的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'}]})
|
中间件处理了改变前和改变后的状态,写法也非常容易理解,如果有业务需要对状态集中处理,通过中间件的方式也不失为一种选择。
文章头部的那张图,如果加上中间件,就是这样:
中间件的顺序
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); }) } }
|
总结
以上是最近一段时间通过在业务不断实践,然后学习和思考的总结。
在这期间发现,很多东西需要事先去积累,要拓宽自己的视野,如果自己不知道某种设计思路并且没有相关经验时,很难想到点上。
技术与业务是相辅相成的,技术能够很好的帮助自己拓宽视野,能设计更好的项目架构;而业务能够让技术得以实践,并且发现技术上可能还存在的问题,从而积累经验。
这个思路我通过这段时间的学习感悟到的,今后的学习也会沿着这个思路走下去。