异步数据流(redux-thunk)
- 网络请求可以在类组件的
componentDidMount
中发送,所以可以有这样的结构:
javascript
componentDidMount() {
axios.get(url).then(res => {
const banners = res.data.banner.list
this.props.changeBanners(banners)
})
}
- 不合理的点: 异步请求的数据是需要放到
Store
中的,也就是和Redux
相关,那么异步请求更改数据应该是属于Redux
的一部分
将异步请求交给
Redux
来管理
dispatch
是个同步操作,并且只能支持传入一个普通对象,如果将actionCreator
改成async
函数,那么他会返回一个Promise
javascript
// 错误操作,这里action生成器返回的不是普通对象
const changeBannersAction = async () => {
const { data:{ data } } = await axios.get(url)
const banners = data.banner.list
return { type: actionTypes.CHANGE_BANNERS, banners }
}
- 如果使用
then
链式操作,则拿不到服务器返回的数据
javascript
const changeBannersAction = () => {
axios.get(url).then(res => {
const banners = res.data.data.banner.list
})
// 这里拿到不banners
return { type: actionTypes.CHANGE_BANNERS, banners }
}
- 这时需要使用官方推荐的库
redux-thunk
对store
增强,让dispatch
可以接收一个函数 - 安装
redux-thunk
shell
npm i redux-thunk
- 使用
redux-thunk
和中间件增强store
javascript
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer, applyMiddleware(thunk))
export default store
- 这时
dispatch
就能接收函数,并且该函数立即调用,action
生成器返回函数
返回的函数携带两个参数
dispatch
: 函数,用于分发action
getState
: 函数,用于获取state
中的数据
javascript
const changeBannersAction = () => async (dispatch, getState) => {
const { data:{ data } } = await axios.get(url)
const banners = data.banner.list
dispatch({type: actionTypes.CHANGE_BANNERS, banners})
}
- 在组件中映射并调用
javascript
class Category extends PureComponent {
componentDidMount() {
this.props.changeBanners()
}
}
const mapActionToProps = (dispatch) => ({
changeBanners(){
dispatch(changeBannersAction())
}
})
const mapStateToProps = (dispatch) => ({
bannners: state.banners
})
export default connect(mapStateToProps, mapActionToProps)(Category)
redux-thunk
优点
- 体积小:
redux-thunk
的实现方式很简单,只有不到 20 行代码 - 使用简单:
redux-thunk
没有引入像redux-saga
或redux-observable
额外的范式,上手简单
redux-thunk
缺陷
- 样板代码过多: 与
Redux
本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的 - 耦合严重: 异步操作与
action
耦合在一起,不方便管理 - 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装
中间件原理
- 中间件底层是对原来的处理做了拦截,利用一个
hack
技术Monkey Patching
,修改原有的程序逻辑 - 例如
dispatch
原本只可以传入一个action
对象,使用thunk
后可以传入函数,其实就是底层拦截了store
的dispatch
的调用过程
javascript
const patchingThunk = (store) => {
const next = store.dispatch;
const patchingDispatch = (action) => {
console.log(action); // 在原来的dispatch之前做其他处理
next(action)
};
store.dispatch = patchingDispatch;
}
patchingThunk(store);
Thunk核心逻辑
- 在调用
dispatch
的过程中,真正调用的函数其实是patchingDispatch
,那么当dispatch
传入函数时就可以做特殊处理
javascript
const patchingThunk = (store) => {
const next = store.dispatch;
const patchingDispatch = (action) => {
if(Object.prototype.toString.call(action) === '[object Function]'){
// 这里的dispacth是patchingDispatch,若再次传入函数也可处理
action(store.dispatch, store.getState)
}else if(Object.prototype.toString.call(action) === '[object Object]' && action.type){
next(action)
}else {
// 其他情况则抛出错误
}
next(action)
};
store.dispatch = patchingDispatch;
}
模块拆分
- 如果将所有更新逻辑都放入到单个
reducer
函数中,项目将变得难以维护,因此,把reducer
拆分成一个个小模块是很好的编程方式 - 每个模块的
reducer
仅负责管理该模块所使用的数据
基本拆分
reducer
更新逻辑拆分
javascript
// store/category/reducer.js
import * as actionTypes from './constant';
const initialState = {
banners: [],
recommends: []
}
function reducer(state = initialState, action) {
switch(action.type){
case actionTypes.CHANGE_BANNERS:
return { ...state, banners: action.banners };
case actionTypes.CHANGE_RECOMMENDS:
return { ...state, recommends: action.recommends };
default:
return state
}
}
export default reducer
actionType
常量拆分
javascript
// store/category/constant.js
const CHANGE_BANNERS = 'change_banners';
const CHANGE_RECOMMENDS= 'change_recommends';
export {
CHANGE_BANNERS,
CHANGE_RECOMMENDS
}
actionCreators
拆分
javascript
// store/category/actionCreators.js
import * as actionTypes from './constant';
import axios from 'axios'; // 举例使用,项目中需要封装
const fetchHomeMultidataAction = () => async (dispatch) => {
const { data:{ data } } = await axios.get(url)
const banners = data.banner.list
const recommends = data.recommend.list
dispatch({ type: actionTypes.CHANGE_BANNERS, banners })
dispatch({ type: actionTypes.CHANGE_RECOMMENDS, recommends })
}
export {
fetchHomeMultidataAction
}
index
文件负责暴露内容
javascript
import reducer from './reducer';
export default reducer; // 暴露自己的reducer
export * from './actionCreators'; // 暴露action生成器
合并reducer
- 当将
reducer
拆分成为多个模块后,拆分之后的reducer
都是相同的结构,并且每个reducer
独立负责管理自身state
的更新 - 那么需要用
Redux
提供的combineReducers
函数,将各模块的reducer
组合成一个Reducer
combineReducers
作用: 把多个不同reducer
作为value
的对象,合并成一个最终的reducer
combineReducers
函数接收一个拆分后的reducer
函数组成的对象
javascript
import { createStore, combineReducers } from "redux";
import counterReducer from './counter';
import categoryReducer from './category';
const reducer = combineReducers({
counter: counterReducer,
category: categoryReducer
})
const store = createStore(reducer)
export default store
- 合并后的
reducer
可调用各模块的reducer
,并将它们的返回结果合并成一个state
对象 - 由
combineReducers
返回的state
,会将传入的每个reducer
返回的state
按对应的key
命名
- 获取
state
数据时,需要按模块名称区分
javascript
const mapStateToProps = (state) => ({
count: state.counter.count, // 获取counter模块的state数据
banners: state.category.banners, // 获取category模块的state数据
recommends: state.category.recommends
})
- 使用
action
需要在对应的模块下引入
javascript
import { mulCountAction } from '../store/counter';
import { fetchHomeMultidataAction } from '../store/category'