这里要讲的就是一个Redux在React中的应用问题,讲一讲Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-saga这些包的做用和他们解决的问题。
由于不想把篇幅拉得太长,因此没有太多源码分析和语法讲解,能怎么简单就怎么简单。react
先看看百度百科上面Redux的一张图:ios
这是Redux在Github上的介绍:Redux用于js程序,是一个可预测的状态容器。git
在这里咱们首先要明白的是什么叫可预测?什么叫状态容器?github
什么叫状态?实际上就是变量,对话框显示或隐藏的变量,一杯奶茶多少钱的变量。redux
那么这个状态容器,实际上就是一个存放这些变量的变量。axios
你建立了一个全局变量叫Store,而后将代码中控制各个状态的变量存放在里面,那么如今Store就叫作状态容器。后端
什么叫可预测?数组
你在操做这个Store的时候,老是用Store.price的方式来设置值,这种操做数据的方式很原始,对于复杂的系统而言永远都不知道程序在运行的过程当中发生了什么。promise
那么如今咱们都经过发送一个Action去作修改,而Store在接收到Action后会使用Reducer对Action传递的数据作处理,最后应用到Store中。app
相对于Store.price的方式来修改者,这种方式无疑更麻烦,可是这种方式的好处就是,每个Action里面均可以写日志,能够记录各类状态的变更,这就是可预测。
因此若是你的程序很简单,你彻底没有必要去用Redux。
看看Redux的示例代码:
actionTypes.js:
export const CHANGE_BTN_TEXT = 'CHANGE_BTN_TEXT';
actions.js:
import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
reducers.js:
import * as T from './actionTypes'; const initialState = { btnText: '我是按钮', }; const pageMainReducer = (state = initialState, action) => { switch (action.type) { case T.CHANGE_BTN_TEXT: return { ...state, btnText: action.payload }; default: return state; } }; export default pageMainReducer;
index.js
import { createStore } from 'redux'; import reducer from './reducers'; import { changeBtnText } from './actions'; const store = createStore(reducer); // 开始监听,每次state更新,那么就会打印出当前状态 const unsubscribe = store.subscribe(() => { console.info(store.getState()); }); // 发送消息 store.dispatch(changeBtnText('点击了按钮')); // 中止监听state的更新 unsubscribe();
这里就不解释什么语法做用了,网上这样的资料太多了。
Redux是一个可预测的状态容器,跟React这种构建UI的库是两个相互独立的东西。
Redux要应用到React中,很明显action,reducer,dispatch这几个阶段并不须要改变,惟一须要考虑的是redux中的状态须要如何传递给react组件。
很简单,只须要每次要更新数据时运用store.getState获取到当前状态,并将这些数据传递给组件便可。
那么问题来了,如何让每一个组件都获取到store呢?
固然是将store做为一个值传递给根组件,而后store就会一级一级往下传,使得每一个组件都能获取到store的值。
可是这样太繁琐了,难道每一个组件须要写一个传递store的逻辑?为了解决这个问题,那么得用到React的context玩法,经过在根组件上将store放在根组件的context中,而后在子组件中经过context获取到store。
react-redux的主要思路也是如此,经过嵌套组件Provider将store放到context中,经过connect这个高阶组件,来隐藏取store的操做,这样咱们就不须要每次去操做context写一大堆代码那么麻烦了。
而后咱们再来基于以前的Redux示例代码给出react-redux的使用演示代码,其中action和reduce部分不变,先增长一个组件PageMain:
const PageMain = (props) => { return ( <div> <button onClick={() => { props.changeText('按钮被点击了'); }} > {props.btnText} </button> </div> ); }; // 映射store.getState()的数据到PageMain const mapStateToProps = (state) => { return { btnText: state.pageMain.btnText, }; }; // 映射使用了store.dispatch的函数到PageMain const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnText(text)); } }; }; // 这个地方也能够简写,react-redux会自动作处理 const mapDispatchToProps = { changeText: changeBtnText }; export default connect(mapStateToProps, mapDispatchToProps)(PageMain);
注意上面的state.pageMain.btnText,这个pageMain是我用redux的combineReducers将多个reducer合并后给的原先的reducer一个命名。
它的代码以下:
import { combineReducers } from 'redux'; import pageMain from './components/pageMain/reducers'; const reducer = combineReducers({ pageMain }); export default reducer;
而后修改index.js:
import React from 'react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import ReactDOM from 'react-dom'; import reducer from './reducers'; import PageMain from './components/pageMain'; const store = createStore(reducer); const App = () => ( <Provider store={store}> <PageMain /> </Provider> ); ReactDOM.render(<App />, document.getElementById('app'));
以前咱们讲到Redux是个可预测的状态容器,这个可预测在于对数据的每一次修改均可以进行相应的处理和记录。
假如如今咱们须要在每次修改数据时,记录修改的内容,咱们能够在每个dispatch前面加上一个console.info记录修改的内容。
可是这样太繁琐了,因此咱们能够直接修改store.dispatch:
let next = store.dispatch store.dispatch = (action)=> { console.info('修改内容为:', action) next(action) }
Redux中也有一样的功能,那就是applyMiddleware。直译过来就是“应用中间件”,它的做用就是改造dispatch函数,跟上面的玩法基本雷同。
来一段演示代码:
import { createStore, applyMiddleware } from 'redux'; import reducer from './reducers'; const store = createStore(reducer, applyMiddleware(curStore => next => action => { console.info(curStore.getState(), action); return next(action); }));
看起来挺奇怪的玩法,可是理解起来并不难。经过这种返回函数的方法,使得applyMiddleware内部以及咱们使用时能够处理store和action,而且这里next的应用就是为了使用多个中间件而存在的。
而一般咱们没有必要本身写中间件,好比日志的记录就已经有了成熟的中间件:redux-logger,这里给一个简单的例子:
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; import reducer from './reducers'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
这样就能够记录全部action及其发送先后的state的日志,咱们能够了解到代码实际运行时到底发生了什么。
在上面的代码中,咱们点击按钮后,直接修改了按钮的文本,这个文本是个固定的值。
actions.js:
import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
可是在咱们实际生产的过程当中,不少状况都是须要去请求服务端拿到数据再修改的,这个过程是一个异步的过程。又或者须要setTimeout去作一些事情。
咱们能够去修改这一部分以下:
const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnText('正在加载中')); axios.get('http://test.com').then(() => { dispatch(changeBtnText('加载完毕')); }).catch(() => { dispatch(changeBtnText('加载有误')); }); } }; };
实际上,咱们天天不知道要处理多少这样的代码。
可是问题来了,异步操做相比同步操做多了一个不少肯定因素,好比咱们展现正在加载中时,可能要先要作异步操做A,而请求后台的过程却很是快,致使加载完毕先出现,而这时候操做A才作完,而后再展现加载中。
因此上面的这个玩法并不能知足这种状况。
这个时候咱们须要去经过store.getState获取当前状态,从而判断究竟是展现正在加载中仍是展现加载完毕。
这个过程就不能放在mapDispatchToProps中了,而须要放在中间件中,由于中间件中能够拿到store。
首先创造store的时候须要应用react-thunk,也就是
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; const store = createStore( reducer, applyMiddleware(thunk) );
它的源码超级简单:
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;
从这个里面能够看出,它就是增强了dispatch的功能,在dispatch一个action以前,去判断action是不是一个函数,若是是函数,那么就执行这个函数。
那么咱们使用起来就很简单了,此时咱们修改actions.js
import axios from 'axios'; import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; }; export const changeBtnTextAsync = (text) => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get(`http://test.com/${text}`).then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
而原来mapDispatchToProps中的玩法和同步action的玩法是同样的:
const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnTextAsync(text)); } }; };
经过redux-thunk咱们能够简单地进行异步操做,而且能够获取到各个异步操做时期状态的值。
Redux虽然好用,可是里面仍是有些重复代码,因此有了redux-actions来简化那些重复代码。
这部分简化工做主要集中在构造action和处理reducers方面。
先来看看原先的actions
import axios from 'axios'; import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; }; export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
而后再来看看修改后的:
import axios from 'axios'; import * as T from './actionTypes'; import { createAction } from 'redux-actions'; export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text); export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
这一块代码替换上面的部分代码后,程序运行结果依然保持不变,也就是说createAction只是对上面的代码进行了简单的封装而已。
这里注意到,异步的action就不要用createAction,由于这个createAction返回的是一个对象,而不是一个函数,就会致使redux-thunk的代码没有起到做用。
这里也可使用createActions这个函数同时建立多个action,可是讲道理,这个语法很奇怪,用createAction就好。
一样redux-actions对reducer的部分也进行了处理,好比handleAction以及handelActions。
先来看看原先的reducers
import * as T from './actionTypes'; const initialState = { btnText: '我是按钮', }; const pageMainReducer = (state = initialState, action) => { switch (action.type) { case T.CHANGE_BTN_TEXT: return { ...state, btnText: action.payload }; default: return state; } }; export default pageMainReducer;
而后使用handleActions来处理
import { handleActions } from 'redux-actions'; import * as T from './actionTypes'; const initialState = { btnText: '我是按钮', }; const pageMainReducer = handleActions({ [T.CHANGE_BTN_TEXT]: { next(state, action) { return { ...state, btnText: action.payload, }; }, throw(state) { return state; }, }, }, initialState); export default pageMainReducer;
这里handleActions能够加入异常处理,而且帮助处理了初始值。
注意,不管是createAction仍是handleAction都只是对代码作了一点简单的封装,二者能够单独使用,并非说使用了createAction就必需要用handleAction。
还记得上面在使用redux-actions的createAction时,咱们对异步的action没法处理。
由于咱们使用createAction后返回的是一个对象,而不是一个函数,就会致使redux-thunk的代码没有起到做用。
而如今咱们将使用redux-promise来处理这类状况。
能够看看以前咱们使用 createAction的例子:
export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);
如今咱们先加入redux-promise中间件:
import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers'; const store = createStore(reducer, applyMiddleware(thunk, createLogger, promiseMiddleware));
而后再处理异步action:
export const changeBtnTextAsync = createAction(T.CHANGE_BTN_TEXT_ASYNC, (text) => { return axios.get(`http://test.com/${text}`); });
能够看到咱们这里返回的是一个Promise对象.(axios的get方法结果就是Promise对象)
咱们还记得redux-thunk中间件,它会去判断action是不是一个函数,若是是就执行。
而咱们这里的redux-promise中间件,他会在dispatch时,判断若是action不是相似
{ type:'', payload: '' }
这样的结构,也就是 FSA,那么就去判断是否为promise对象,若是是就执行action.then的玩法。
很明显,咱们createAction后的结果是FSA,因此会走下面这个分支,它会去判断action.payload是否为promise对象,是的话那就
action.payload .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
也就是说咱们的代码最后会转变为:
axios.get(`http://test.com/${text}`) .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
这个中间件的代码也很简单,总共19行,你们能够在github上直接看看。
咱们的异步处理用的是redux-thunk + redux-actions + redux-promise,其实用起来仍是蛮好用的。
可是随着ES6中Generator的出现,人们发现用Generator处理异步能够更简单。
而redux-saga就是用Generator来处理异步。
如下讲的知识是基于Generator的,若是您对这个不甚了解,能够简单了解一下相关知识,大概须要2分钟时间,并不难。
redux-saga文档并无说本身是处理异步的工具,而是说用来处理边际效应(side effects),这里的边际效应你能够理解为程序对外部的操做,好比请求后端,好比操做文件。
redux-saga一样是一个redux中间件,它的定位就是经过集中控制action,起到一个相似于MVC中控制器的效果。
同时它的语法使得复杂异步操做不会像promise那样出现不少then的状况,更容易进行各种测试。
这个东西有它的好处,一样也有它很差的地方,那就是比较复杂,有必定的学习成本。
而且我我的而言很不习惯Generator的用法,以为Promise或者await更好用。
这里仍是记录一下用法,毕竟有不少框架都用到了这个。
应用这个中间件和咱们的其余中间件没有区别:
import React from 'react'; import { createStore, applyMiddleware } from 'redux'; import promiseMiddleware from 'redux-promise'; import createSagaMiddleware from 'redux-saga'; import {watchDelayChangeBtnText} from './sagas'; import reducer from './reducers'; const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(promiseMiddleware, sagaMiddleware)); sagaMiddleware.run(watchDelayChangeBtnText);
建立sage中间件后,而后再将其中间件接入到store中,最后须要用中间件运行sages.js返回的Generator,监控各个action。
如今咱们给出sages.js的代码:
import { delay } from 'redux-saga'; import { put, call, takeEvery } from 'redux-saga/effects'; import * as T from './components/pageMain/actionTypes'; import { changeBtnText } from './components/pageMain/actions'; const consoleMsg = (msg) => { console.info(msg); }; /** * 处理编辑效应的函数 */ export function* delayChangeBtnText() { yield delay(1000); yield put(changeBtnText('123')); yield call(consoleMsg, '完成改变'); } /** * 监控Action的函数 */ export function* watchDelayChangeBtnText() { yield takeEvery(T.WATCH_CHANGE_BTN_TEXT, delayChangeBtnText); }
在redux-saga中有一类用来处理边际效应的函数好比put、call,它们的做用是为了简化操做。
好比put至关于redux的dispatch的做用,而call至关于调用函数。(能够参考上面代码中的例子)
还有另外一类函数就是相似于takeEvery,它的做用就是和普通redux中间件同样拦截到action后做出相应处理。
好比上面的代码就是拦截到T.WATCH_CHANGE_BTN_TEXT这个类型的action,而后调用delayChangeBtnText。
而后能够回看咱们以前的代码,有这么一行代码:
sagaMiddleware.run(watchDelayChangeBtnText);
这里实际就是引入监控的这个生成器后,再运行监控生成器。
这样咱们在代码里面dispatch类型为T.WATCH_CHANGE_BTN_TEXT的action时就会被拦截而后作出相应处理。
固然这里有人可能会提出疑问,难道每个异步都要这么写吗,那岂不是要run不少次?
固然不是这个样子,咱们能够在sage中这么写:
export default function* rootSaga() { yield [ watchDelayChangeBtnText(), watchOtherAction() ] }
咱们只须要按照这个格式去写,将watchDelayChangeBtnText这样用于监控action的生成器放在上面那个代码的数组中,而后做为一个生成器返回。
如今只须要引用这个rootSaga便可,而后run这个rootSaga。
之后若是要监控更多的action,只须要在sages.js中加上新的监控的生成器便可。
经过这样的处理,咱们就将sages.js作成了一个像MVC中的控制器的东西,能够用来处理各类各样的action,处理复杂的异步操做和边际效应。
可是这里要注意,必定要加以区分sages.js中使用监控的action和真正功能用的action,好比加个watch关键字,以避免业务复杂后代码混乱。
总的来讲:
OK,虽说不想写那么多,结果仍是写了一大堆。
若是您以为对您还有帮助,那么也请点个赞吧。