常用链接
redux 是 js 应用的可预测状态的容器。 可以理解为全局数据状态管理工具(状态管理机),用来做组件通信等。
React Redux 官方网址: https://react-redux.js.org/
中文文档:https://cn.redux.js.org
https://github.com/reduxjs/redux
https://github.com/reduxjs/react-redux
https://github.com/reduxjs/redux-thunk
简单举例说明
一个学校有4套“春夏秋冬”校服,学生怎么知道什么时候该穿什么样的校服?
- 保安亭(store) 里面放着保安(reducer)、里面窗户上挂着衣服(state)
- 同学们(订阅者) 只有看服装的权限,没有改服装的权限(安全)
- 校长(发布者) 派发动作“改衣服”给保安,告诉他要换“秋装”了;
- 保安接收后,就把挂着的衣服换成“秋装”,同时通知同学们;
- 同学们就去看现在的服装是什么样子,然后第2天,就穿着秋装来上学了。

Redux 应用场景
- 随着 Javascript 单页面应用开发日趋复杂,管理不断变化的 state 非常困难。
- Redux 的出现就是为了解决 state 里的数据问题。
- 在 React 中,数据在组件中是单向流动的。
- 数据从一个方向父组件流向子组件(通过props),由于这个特征,两个非父子关系的组件之间(兄弟组件)的通信就比较麻烦。

Redux 设计思想
- Redux 是将整个应用状态存储到一个地方,称为 store。
- 里面保存一颗状态数 state tree。
- 组件可以派发 dispatch 行为 action 给 store,而不是直接通过其它组件。
- 其它组件可以通过订阅 store 中的状态(state),来刷新自己的视图。

Redux 三大原则
- 整个应用的 state 被存储在一颗 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State 是只读的,唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象,使用纯函数来执行修改,为了描述 action 如何改变 state tree,需要编写 reducers。
- 单一数据源的设计让 React 的组件之间的通信更加方便,同时也便于状态的统一管理。
安装
cnpm i -S redux react-reduxcnpm i -D redux-devtools # 开发者工具
redux
createStore 创建store仓库
createStore(reducer, initState)- 第一个参数:处理器;第二个参数:状态的初始值
- https://github.com/reduxjs/redux/blob/master/src/createStore.ts
原生计数器
public/index.html
<body><div id="root"></div><div id="counter"><p id="count-value">0</p><button id="add-btn">add</button><button id="minus-btn">minus</button></div></body>
src/index.js
import { createStore } from 'redux';let countValue = document.querySelector('#count-value');let addBtn = document.querySelector('#add-btn');let minusBtn = document.querySelector('#minus-btn');let initState = {number: 0};/** reducer 处理器** @param {*} state 老状态* @param {*} action 动作,里面一般有2参数:type(类型)、payload(额外参数)* @returns 新状态*/const reducer = (state=initState, action) => {switch (action.type){ //根据不同动作类型,进行其各自的处理case 'add':// return {number: state.number + action.payload};return {number: state.number + 1};case 'minus':return {number: state.number - 1};default:return state;}}// 创建store仓库(第一个参数:处理器;第二个参数:状态的初始值)// let store = createStore(reducer, initState);let store = createStore(reducer);function render(){let state = store.getState();countValue.innerHTML = JSON.stringify(state);}render(); //初始渲染一次// 订阅 store.subscribe (该函数没有参数)store.subscribe((...args) => {console.log('args=>', args); // args=> []render();})addBtn.addEventListener('click', () => {// store.dispatch({type: 'add', payload: 5}); // 派发actionstore.dispatch({type: 'add'});})minusBtn.addEventListener('click', () => {store.dispatch({type: 'minus'});})
React 计数器
src/index.js
import React from 'react';import ReactDOM from 'react-dom';import Counter1 from './components/Counter1'ReactDOM.render(<Counter1 />, document.getElementById('root'));
src/components/Counter1.js
import React from 'react';import {createStore} from 'redux';const reducer = (state=initState, action) => {switch (action.type){case 'add':return {number: state.number + action.payload};case 'minus':return {number: state.number - 1};default:return state;}}let initState = {number: 10};let store = createStore(reducer, initState);class Counter1 extends React.Component{state={number: store.getState().number};componentDidMount(){this.unsubscribe = store.subscribe(() => {this.setState({number: store.getState().number});})}componentWillUnmount(){this.unsubscribe();}add = () => {store.dispatch({type: 'add', payload: 5});}minus = () => {store.dispatch({type: 'minus'});}render (){return (<div><p>{this.state.number}</p><button onClick={this.add}>add</button><button onClick={this.minus}>minus</button></div>)}}export default Counter1;
bindActionCreators 绑定action创建者
https://github.com/reduxjs/redux/blob/master/src/bindActionCreators.ts
src/components/Counter1.js
import React from 'react';+ import {createStore, bindActionCreators} from 'redux';const reducer = (state=initState, action) => {switch (action.type){case 'add':return {number: state.number + 1};case 'minus':return {number: state.number - 1};default:return state;}}let initState = {number: 10};let store = createStore(reducer, initState);+ // 创建 action+ const add = () => ({type: 'add'});+ const minus = () => ({type: 'minus'});+ // 创建一个 actionCreator 对象+ const actions = {add, minus};+ // 绑定 actionCreator+ // 绑定之后 bindActions.add 就变成了 () => store.dispatch({type: 'add'})+ const bindActions = bindActionCreators(actions, store.dispatch);class Counter1 extends React.Component{state={number: store.getState().number};componentDidMount(){this.unsubscribe = store.subscribe(() => {this.setState({number: store.getState().number});})}componentWillUnmount(){this.unsubscribe();}render (){return (<div><p>{this.state.number}</p>+ <button onClick={bindActions.add}>add</button>+ <button onClick={bindActions.minus}>minus</button></div>)}}export default Counter1;
combineReducers
https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts
- redux应用中,reducer、store、state都只有一个,都是单例的。
- 如果应用比较庞大的话,肯定要写很多 reducer,然后再合并成一个 reducer。
src/index.js
import React from 'react';import ReactDOM from 'react-dom';import Counter1 from './components/Counter1'import Counter2 from './components/Counter2'ReactDOM.render(<div><Counter1 /><Counter2 /></div>,document.getElementById('root'));
src/store/index.js 仓库
import {createStore} from 'redux';import rootReducer from './reducers';const store = createStore(rootReducer); //rootReducer 就是根reducerexport default store;
src/store/action-types 动作类型
export const ADD1 = 'ADD1';export const MINUS1 = 'MINUS1';export const ADD2 = 'ADD2';export const MINUS2 = 'MINUS2';export const ALLMINUS = 'ALLMINUS';
src/store/reduces 合并所有的分reducer
src/store/reducers/index.js
import {combineReducers} from 'redux';import counter1 from './counter1';import counter2 from './counter2';// 合并所有的分 reducerlet reducers = {counter1, counter2};let rootReducer = combineReducers(reducers);export default rootReducer;
src/store/reducers/counter1.js
import * as types from '../action-types.js';// 这是 Counter1 组件对应的分 reducer,它有自己独立的状态const counter1 = (state={number:5}, action) => {switch (action.type){case types.ADD1:return {number: state.number + 1};case types.MINUS1:return {number: state.number - 1};case types.ALLMINUS:return {number: state.number - 1};default:return state;}}export default counter1;
src/store/reducers/counter2.js
import * as types from '../action-types.js';// 这是 Counter2 组件对应的分 reducer,它有自己独立的状态const counter2 = (state={number:10}, action) => {switch (action.type){case types.ADD2:return {number: state.number + 1};case types.MINUS2:return {number: state.number - 1};case types.ALLMINUS:return {number: state.number - 1};default:return state;}}export default counter2;
src/store/actions 动作对象
src/store/actions/counter1.js
import * as types from '../action-types';const actions = {add1: () => ({type: types.ADD1}),minus1: () => ({type: types.MINUS1}),}export default actions;
src/store/actions/counter2.js
import * as types from '../action-types';const actions = {add2: () => ({type: types.ADD2}),minus2: () => ({type: types.MINUS2}),allMinus: () => ({type: types.ALLMINUS}),}export default actions;
src/components
可以看出组件里还是有很多重复的逻辑处理,这就需要用 react-redux 的connect 高阶组件去优化处理。
src/components/Counter1.js
import React from 'react';import {bindActionCreators} from 'redux';+ import store from '../store';+ import actions from '../store/actions/counter1';const bindActions = bindActionCreators(actions, store.dispatch);class Counter1 extends React.Component{+ state={number: store.getState().counter1.number};componentDidMount(){this.unsubscribe = store.subscribe(() => {+ this.setState({number: store.getState().counter1.number});})}componentWillUnmount(){this.unsubscribe();}render (){return (<div><p>{this.state.number}</p>+ <button onClick={bindActions.add1}>add1</button>+ <button onClick={bindActions.minus1}>minus1</button></div>)}}export default Counter1;
src/components/Counter2.js
import React from 'react';import {bindActionCreators} from 'redux';+ import store from '../store';+ import actions from '../store/actions/counter2';const bindActions = bindActionCreators(actions, store.dispatch);class Counter2 extends React.Component{+ state={number: store.getState().counter2.number};componentDidMount(){this.unsubscribe = store.subscribe(() => {+ this.setState({number: store.getState().counter2.number});})}componentWillUnmount(){this.unsubscribe();}render (){return (<div><p>{this.state.number}</p>+ <button onClick={bindActions.add2}>add2</button>+ <button onClick={bindActions.minus2}>minus2</button>+ <button onClick={bindActions.allMinus}>allMinus</button></div>)}}export default Counter2;
react-redux
Provider
- https://github.com/reduxjs/react-redux/blob/master/src/components/Provider.js
- 其实就是 react-redux 的 context 上下文。
- 接收一个 store 属性并向下层组件传递。
src/index.js
```jsx import React from ‘react’; import ReactDOM from ‘react-dom’; import {Provider} from ‘react-redux’; import store from ‘./store’; import Counter1 from ‘./components/Counter1’ import Counter2 from ‘./components/Counter2’
ReactDOM.render(
<a name="qHSeZ"></a>## connect- [https://github.com/reduxjs/react-redux/tree/master/src/connect](https://github.com/reduxjs/react-redux/tree/master/src/connect)- 负责把仓库和组件进行关联<a name="dklzG"></a>##### src/components/Counter1.js```jsximport React from 'react';import {connect} from 'react-redux';import actions from '../store/actions/counter1';import * as types from '../store/action-types';class Counter1 extends React.Component{render (){let ps = this.props;return (<div><p>{ps.number}</p><button onClick={ps.add1}>add1</button><button onClick={ps.minus1}>minus1</button></div>)}}// 语法:connect(mapStateToProps, mapDispatchToProps, ...)// 第一个参数 mapStateToProps:状态映射函数。// 接收仓库的总状态,返回指定的分状态, connect再把函数返回值映射为组件属性function mapStateToProps(state){return state.counter1;}// 第二个参数 mapDispatchToProps:主要把经过 bingActionCreators 绑定后的 actions对象 映射为属性。// 大致有7中写法// 第1种,传函数。接收 store.dispatch 为参数,返回绑定后的 actions对象function mapDispatchToProps(dispatch){return {add1: () => dispatch({type: types.ADD1}),minus1: () => dispatch({type: types.MINUS1}),}}export default connect(mapStateToProps, mapDispatchToProps)(Counter1);// 第2种:直接传未经过绑定的 actions对象// export default connect(mapStateToProps, actions)(Counter1);
src/components/Counter2.js
import React from 'react';import {connect} from 'react-redux';import actions from '../store/actions/counter2';class Counter2 extends React.Component{render (){let ps = this.props;return (<div><p>{ps.number}</p><button onClick={ps.add2}>add2</button><button onClick={ps.minus2}>minus2</button><button onClick={ps.allMinus}>allMinus</button></div>)}}function mapStateToProps({counter2}){return counter2;}export default connect(mapStateToProps, actions)(Counter2);
hooks (useDispatch、useSelector、useState)
新版的react-redux为了更好的用于函数组件,提供了一些 hooks。
- https://github.com/reduxjs/react-redux/tree/master/src/hooks
useDispatch获取 store.dispatchuseSelector状态映射函数 mapStateToProps(state => currentComponentState)useStore获取仓库 store
src/components/Counter1.js
import React from 'react';import {useDispatch, useSelector, useStore} from 'react-redux';import * as types from '../store/action-types';function Counter1(){let dispatch = useDispatch(); //store.dispatchlet state = useSelector(state => state.counter1); //获取状态let store = useStore(); //获取仓库 {dispatch, getState, subscribe}return (<div><p>{state.number}</p><button onClick={() => dispatch({type: types.ADD1})}>add1</button><button onClick={() => dispatch({type: types.MINUS1})}>minus1</button></div>)}export default Counter1;
applyMiddleware(redux中间件)
简述
- 如果没有中间件的运用,redux的工作流程就是
action -> reducer,这就相当于是同步操作,由dispatch触发action后,直接去reducer执行相应的动作。 - 但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决问题。比如有一个这样的需求:点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时就需要引入中间件改变redux同步执行的流程,形成异步流程来实现所要的逻辑,有了中间件,redux的工作流程就变成:action -> middlewares -> reducer,点击按钮就相当于dispatch触发action,接下去获取服务器数据middlewares的执行,当middlewares成功获取到服务器数据就去触发reducer对应的动作,更新需要渲染视图的数据。
- 中间件的机制可以让我们改变数据流,实现异步action,action过滤,日志输出,异常报告等功能。


日志中间件
- 实现一个日志中间件,在更改状态时,打印前后的状态。
- 直接通过改写 dispatch 方法,实现在更改状态时,打印前后的状态。
- 但这种方案并不好
实现日志
src/index.js
import store from './store';store.subscribe(() => console.log('state =>', store.getState()));store.dispatch({type: 'ADD1'});
src/store/index.js
import {createStore} from 'redux';import rootReducer from './reducers';const store = createStore(rootReducer);// 先获取原生的dispatch方法let dispatch = store.dispatch;// 再改写 dispatch 方法,实现在更改状态时,打印前后的状态store.dispatch = function(action){console.log('prev state', store.getState());dispatch(action);console.log('next state', store.getState());return action;}export default store;

实现异步
import {createStore} from 'redux';import rootReducer from './reducers';const store = createStore(rootReducer);let dispatch = store.dispatch;store.dispatch = function(action){setTimeout(() => {dispatch(action);}, 3000);return action;}export default store;

单个日志中间件
- 实现一个真正的日志中间件
src/index.js
```jsx import store from ‘./store’; store.subscribe(() => console.log(‘state =>’, store.getState()));
// 派发普通对象 store.dispatch({type: ‘ADD1’});
// 派发函数 // store.dispatch((dispatch, getState) => { // setTimeout(() => { // dispatch({type: ‘ADD1’}); //这里可以继续派发函数 // }, 3000) // });
// 派发promise // store.dispatch(new Promise((resolve, reject) => { // setTimeout(() => { // resolve({type: ‘ADD1’}); // }, 3000) // }));
<a name="nxzlX"></a>##### src/store/index.js```jsximport {createStore} from 'redux';import rootReducer from './reducers';function applyMiddleware(middleware){return function(createStore){return function(reducer){let store = createStore(reducer); //先创建原生的storelet dispatch; //指向改造后的dispatch方法let middlewareAPI = {getState: store.getState,dispatch: action => dispatch(action),}dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法return {...store, dispatch};}}}// 日志中间件的真正实现(只要是中间件,格式都是定死的)function logger(store){return function(next){return function(action){console.log('prev state', store.getState());next(action);console.log('next state', store.getState());}}}const store = applyMiddleware(logger)(createStore)(rootReducer);export default store;
单个支持派发函数、promise的中间件
支持函数
// 原生的 store.dispatch 只支持传递普通对象,不能接受函数,也不能接收promise。// 为了要让其支持,添加 支持函数的中间件function thunk(store){return function(next){return function(action){// 如果派发的action是一个函数,就让它执行if (typeof action === 'function'){// 如果这里第一个参数传递next(即原生的store.dispatch方法),// 那么函数内 dispatch 如果继续派发函数,那就报错了。return action(store.dispatch, store.getState);}// 如果不是函数,则意味着不需要自己处理。直接调用下一个dispatch方法即可。return next(action);}}}const store = applyMiddleware(thunk)(createStore)(rootReducer);
支持promise
// 添加支持promise的中间件function promise(store){return function(next){return function(action){if (typeof action.then === 'function'){return action.then(newAction => store.dispatch(newAction));}return next(action);}}}const store = applyMiddleware(promise)(createStore)(rootReducer);
级联中间件
- 上面都是单一的中间件,但是真正项目里假如有很多中间件,怎么办呢?
- 在真实的项目中,中间件每个逻辑都是单独编写的,但是可以向 applyMiddleware传递多个中间件
const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);

function applyMiddleware(...middlewares){return function(createStore){return function(reducer){let store = createStore(reducer); //先创建原生的storelet dispatch; //指向改造后的dispatch方法let middlewareAPI = {getState: store.getState,dispatch: action => dispatch(action),}// dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法let chain = middlewares.map(item => item(middlewareAPI));let [promise, thunk, logger] = chain;dispatch = promise(thunk(logger(store.dispatch)));return {...store, dispatch};}}}const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);
看起来,是不是有点洋葱模型的感觉了,这里就引出来了 compose 管道。
compose、applyMiddleware 的具体实现见下一节 redux-源码篇。
示例
src/store/index.js
import {createStore, applyMiddleware} from 'redux';import reduxLogger from 'redux-logger';import reduxThunk from 'redux-thunk';import reduxPromise from 'redux-promise';import rootReducer from './reducers';const store = applyMiddleware(reduxPromise,reduxThunk,reduxLogger)(createStore)(rootReducer);export default store;
src/components/Couter1.js
import React from 'react';import {useDispatch, useSelector, useStore} from 'react-redux';import * as types from '../store/action-types';function Counter1(){let dispatch = useDispatch(); //store.dispatchlet state = useSelector(state => state.counter1); //获取状态let store = useStore(); //获取仓库 {dispatch, getState, subscribe}return (<div><p>{state.number}</p>{/* 派发参数为 普通对象 */}<button onClick={() => dispatch({type: types.ADD1})}>add1</button><button onClick={() => dispatch({type: types.MINUS1})}>minus1</button>{/* 派发参数为 函数 */}<button onClick={() => dispatch((resDispatch, resGetState) => {setTimeout(() => {resDispatch({type: types.ADD1});}, 2000)})}>function +</button>{/* 派发参数为 Promise */}<button onClick={() => dispatch(new Promise((resolve) => {setTimeout(() => {resolve({type: types.MINUS1});}, 2000)}))}>Promise -</button></div>)}export default Counter1;
