本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
在之前简单的案例中,redux中保存的counter
是一个本地定义的数据,我们可以直接通过同步的操作来dispatch action
,state
就会被立即更新。
但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。
在之前学习网络请求的时候我们讲过,网络请求可以在class
组件的componentDidMount
中发送,所以我们可以有这样的结构:
我现在完成如下案例操作:
Home
组件中请求banners
和recommends
的数据;Profile
组件中展示banners
和recommends
的数据;redux代码进行如下修改:
在reducer.js
中添加state
初始化数据和reducer
函数中处理代码:
const initialState = {
counter: 0,
banners: [],
recommends: []
}
function reducer(state = initialState, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
case SUB_NUMBER:
return { ...state, counter: state.counter - action.num };
case CHANGE_BANNER:
return { ...state, banners: action.banners };
case CHANGE_RECOMMEND:
return { ...state, recommends: action.recommends };
default:
return state;
}
}
constants
中增加常量:
const CHANGE_BANNER = "CHANGE_BANNER";
const CHANGE_RECOMMEND = "CHANGE_RECOMMEND";
actionCreators.js
中添加actions
:
const changeBannersAction = (banners) => ({
type: CHANGE_BANNER,
banners
})
const changeRecommendsAction = (recommends) => ({
type: CHANGE_RECOMMEND,
recommends
})
组件中代码代码修改:
Home
组件:
import React, { PureComponent } from 'react';
import { connect } from "react-redux";
import axios from 'axios';
import {
addAction,
changeBannersAction,
changeRecommendsAction
} from '../store/actionCreators';
class Home extends PureComponent {
componentDidMount() {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
const data = res.data.data;
this.props.changeBanners(data.banner.list);
this.props.changeRecommends(data.recommend.list);
})
}
...其他业务代码
}
const mapStateToProps = state => {
return {
counter: state.counter
}
}
const mapDispatchToProps = dispatch => {
return {
addNumber: function(number) {
dispatch(addAction(number));
},
changeBanners(banners) {
dispatch(changeBannersAction(banners));
},
changeRecommends(recommends) {
dispatch(changeRecommendsAction(recommends));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Profile
组件:
import React, { PureComponent } from 'react';
import { connect } from "react-redux";
import {
subAction
} from '../store/actionCreators';
class Profile extends PureComponent {
render() {
return (
<div>
Profile
<div>
<h2>当前计数: {this.props.counter}</h2>
<button onClick={e => this.decrement()}>-1</button>
<button onClick={e => this.subCounter()}>-5</button>
</div>
<h1>Banners</h1>
<ul>
{
this.props.banners.map((item, index) => {
return <li key={item.acm}>{item.title}</li>
})
}
</ul>
<h1>Recommends</h1>
<ul>
{
this.props.recommends.map((item, index) => {
return <li key={item.acm}>{item.title}</li>
})
}
</ul>
</div>
)
}
...其他逻辑代码
}
const mapStateToProps = state => {
return {
counter: state.counter,
banners: state.banners,
recommends: state.recommends
}
}
const mapDispatchToProps = dispatch => {
return {
subNumber: function (number) {
dispatch(subAction(number));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
上面的代码有一个缺陷:
但是在redux中如何可以进行异步的操作呢?
cookie
解析、日志记录、文件压缩等操作;redux也引入了中间件(Middleware)的概念:
dispatch
的action
和最终达到的reducer
之间,扩展一些自己的代码;我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
redux-thunk
;redux-thunk
是如何做到让我们可以发送异步的请求呢?
dispatch(action)
,action
需要是一个JavaScript的对象;redux-thunk
可以让dispatch(action函数)
,action
可以是一个函数;dispatch
函数和getState
函数;
dispatch
函数用于我们之后再次派发action
;getState
函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;如何使用 redux-thunk 呢?
yarn add redux-thunk
store
时传入应用了middleware
的enhance
函数applyMiddleware
来结合多个Middleware
, 返回一个enhancer
;enhancer
作为第二个参数传入到createStore
中;// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = applyMiddleware(thunkMiddleware);
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);
action
:const getHomeMultidataAction = () => {
return (dispatch) => {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendsAction(data.recommend.list));
})
}
}
dispatch
之后会被执行;home.js
中的代码:import React, { PureComponent } from 'react';
import { connect } from "react-redux";
import {
addAction,
getHomeMultidataAction
} from '../store/actionCreators';
class Home extends PureComponent {
componentDidMount() {
this.props.getHomeMultidata();
}
...其他逻辑代码
}
...mapStatetoProps
const mapDispatchToProps = dispatch => {
return {
addNumber: function(number) {
dispatch(addAction(number));
},
getHomeMultidata() {
dispatch(getHomeMultidataAction());
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
redux-devtools
的工具;安装该工具需要两步:
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducer.js';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware));
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);
export default store;
trace打开:
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
saga中间件使用了ES6的generator
语法,所以我们有必须简单讲解一下:
generator
的所有用法,事实上它的用法非常的灵活,大家可以自行去学习一下。在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果:
function foo() {
return "Hello World";
}
foo() // Hello World
如果我们将这个函数编写成一个生成器函数:
function *foo() {
yield "Hello";
yield "World";
}
const iterator = foo();
console.log(iterator, typeof iterator); // 一个object类型的iterator对象
调用iterator
的next
函数,会销毁一次迭代器,并且返回一个yield
的结果:
// 调用一次next()是消耗一次迭代器
iterator.next(); // {value: "Hello", done: false}
iterator.next(); // {value: "World", done: false}
iterator.next(); // {value: undefined, done: true}
研究一下foo
生成器函数代码的执行顺序:
function *foo() {
console.log("111111");
yield "Hello";
console.log("222222");
yield "World";
console.log("333333");
}
// 调用一次next()是消耗一次迭代器
iterator.next(); // {value: "Hello", done: false}
// 打印111111
iterator.next(); // {value: "World", done: false}
// 打印222222
iterator.next(); // {value: undefined, done: true}
// 打印333333
generator
和promise
一起使用:
function *bar() {
const result = yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hello Generator");
return "Hello";
}, 2000);
});
console.log(result);
}
const bIterator = bar();
bIterator.next().value.then(res => {
bIterator.next(res);
});
yarn add redux-saga
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer.js';
import mySaga from './saga';
// 通过createSagaMiddleware函数来创建saga中间件
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware, sagaMiddleware));
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);
// 必须启动saga中间件,并且传入其要监听的generator
sagaMiddleware.run(mySaga);
export default store;
saga.js
文件的编写:import { takeEvery, put, all } from 'redux-saga/effects';
import axios from 'axios';
import {
FETCH_HOME_MULTIDATA
} from "./constants";
import {
changeBannersAction,
changeRecommendsAction,
} from './actionCreators';
function* fetchHomeMultidata(action) {
const res = yield axios.get("http://123.207.32.32:8000/home/multidata");
console.log(res);
const data = res.data.data;
yield all([
put(changeBannersAction(data.banner.list)),
put(changeRecommendsAction(data.recommend.list))
])
}
function* mySaga() {
yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
}
export default mySaga;
takeEvery
:可以传入多个监听的actionType
,每一个都可以被执行(对应有一个takeLastest
,会取消前面的)put
:在saga中派发action
不再是通过dispatch
,而是通过put
;all
:可以在yield
的时候put
多个action
;前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:
dispatch
之前,打印一下本次的action
对象,dispatch
完成之后可以打印一下最新的store state
;dispatch
都可以包含这样的操作;如果没有中间件,我们是否可以实现类似的代码呢?
当然可以,类似下面的方式即可:
console.log("dispatching:", addAction(5));
store.dispatch(addAction(5));
console.log("new state:", store.getState());
console.log("dispatching:", addAction(10));
store.dispatch(subAction(10));
console.log("new state:", store.getState());
但是这种方式缺陷非常明显:
dispatch
操作,我们都需要在前面加上这样的逻辑代码;是否有一种更优雅的方式来处理这样的相同逻辑呢?
function dispatchAndLog(action) {
console.log("dispatching:", action);
store.dispatch(addAction(5));
console.log("新的state:", store.getState());
}
dispatchAndLog(addAction(10));
但是这样的代码有一个非常大的缺陷:
dispatch
时,必须使用我另外封装的一个函数dispatchAndLog
;dispatch
;我们来进一步对代码进行优化;
事实上,我们可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
我们对代码进行如下的修改:
let next = store.dispatch;
function dispatchAndLog(action) {
console.log("dispatching:", addAction(10));
next(addAction(5));
console.log("新的state:", store.getState());
}
store.dispatch = dispatchAndLog;
dispatch
的调用过程;dispatch
的过程中,真正调用的函数其实是dispatchAndLog
;当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store
进行这样的处理:
function patchLogging(store) {
let next = store.dispatch;
function dispatchAndLog(action) {
console.log("dispatching:", action);
next(addAction(5));
console.log("新的state:", store.getState());
}
store.dispatch = dispatchAndLog;
}
redux-thunk
的作用:
redux-thunk
可以让我们的dispatch
不再只是处理对象,并且可以处理函数;redux-thunk
中的基本实现过程是怎么样的呢?事实上非常的简单。我们来看下面的代码:
function patchThunk(store) {
let next = store.dispatch;
function dispatchAndThunk(action) {
if (typeof action === "function") {
action(store.dispatch, store.getState);
} else {
next(action);
}
}
store.dispatch = dispatchAndThunk;
}
dispatch
进行转换,这个dispatch
会判断传入的将两个patch
应用起来,进行测试:
patchLogging(store);
patchThunk(store);
store.dispatch(addAction(10));
function getData(dispatch) {
setTimeout(() => {
dispatch(subAction(10));
}, 1000)
}
// 传入函数
store.dispatch(getData);
单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice();
middlewares.forEach(middleware => {
store.dispatch = middleware(store);
})
}
applyMiddleware(store, [patchLogging, patchThunk]);
我们来理解一下上面操作之后,代码的流程:
当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考redux合并中间件的源码流程。