常用链接

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天,就穿着秋装来上学了。

Snipaste_2021-05-19_00-39-19.png

Redux 应用场景

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

redux-wrong.png

Redux 设计思想

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

redux-flow.png

Redux 三大原则

  • 整个应用的 state 被存储在一颗 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  • State 是只读的,唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象,使用纯函数来执行修改,为了描述 action 如何改变 state tree,需要编写 reducers。
  • 单一数据源的设计让 React 的组件之间的通信更加方便,同时也便于状态的统一管理。

安装

  1. cnpm i -S redux react-redux
  2. cnpm i -D redux-devtools # 开发者工具

redux

createStore 创建store仓库

原生计数器

rwe.gif

public/index.html
  1. <body>
  2. <div id="root"></div>
  3. <div id="counter">
  4. <p id="count-value">0</p>
  5. <button id="add-btn">add</button>
  6. <button id="minus-btn">minus</button>
  7. </div>
  8. </body>

src/index.js
  1. import { createStore } from 'redux';
  2. let countValue = document.querySelector('#count-value');
  3. let addBtn = document.querySelector('#add-btn');
  4. let minusBtn = document.querySelector('#minus-btn');
  5. let initState = {number: 0};
  6. /** reducer 处理器
  7. *
  8. * @param {*} state 老状态
  9. * @param {*} action 动作,里面一般有2参数:type(类型)、payload(额外参数)
  10. * @returns 新状态
  11. */
  12. const reducer = (state=initState, action) => {
  13. switch (action.type){ //根据不同动作类型,进行其各自的处理
  14. case 'add':
  15. // return {number: state.number + action.payload};
  16. return {number: state.number + 1};
  17. case 'minus':
  18. return {number: state.number - 1};
  19. default:
  20. return state;
  21. }
  22. }
  23. // 创建store仓库(第一个参数:处理器;第二个参数:状态的初始值)
  24. // let store = createStore(reducer, initState);
  25. let store = createStore(reducer);
  26. function render(){
  27. let state = store.getState();
  28. countValue.innerHTML = JSON.stringify(state);
  29. }
  30. render(); //初始渲染一次
  31. // 订阅 store.subscribe (该函数没有参数)
  32. store.subscribe((...args) => {
  33. console.log('args=>', args); // args=> []
  34. render();
  35. })
  36. addBtn.addEventListener('click', () => {
  37. // store.dispatch({type: 'add', payload: 5}); // 派发action
  38. store.dispatch({type: 'add'});
  39. })
  40. minusBtn.addEventListener('click', () => {
  41. store.dispatch({type: 'minus'});
  42. })

React 计数器

src/index.js
  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import Counter1 from './components/Counter1'
  4. ReactDOM.render(<Counter1 />, document.getElementById('root'));

src/components/Counter1.js
  1. import React from 'react';
  2. import {createStore} from 'redux';
  3. const reducer = (state=initState, action) => {
  4. switch (action.type){
  5. case 'add':
  6. return {number: state.number + action.payload};
  7. case 'minus':
  8. return {number: state.number - 1};
  9. default:
  10. return state;
  11. }
  12. }
  13. let initState = {number: 10};
  14. let store = createStore(reducer, initState);
  15. class Counter1 extends React.Component{
  16. state={number: store.getState().number};
  17. componentDidMount(){
  18. this.unsubscribe = store.subscribe(() => {
  19. this.setState({number: store.getState().number});
  20. })
  21. }
  22. componentWillUnmount(){
  23. this.unsubscribe();
  24. }
  25. add = () => {
  26. store.dispatch({type: 'add', payload: 5});
  27. }
  28. minus = () => {
  29. store.dispatch({type: 'minus'});
  30. }
  31. render (){
  32. return (
  33. <div>
  34. <p>{this.state.number}</p>
  35. <button onClick={this.add}>add</button>
  36. <button onClick={this.minus}>minus</button>
  37. </div>
  38. )
  39. }
  40. }
  41. export default Counter1;

bindActionCreators 绑定action创建者

https://github.com/reduxjs/redux/blob/master/src/bindActionCreators.ts

src/components/Counter1.js
  1. import React from 'react';
  2. + import {createStore, bindActionCreators} from 'redux';
  3. const reducer = (state=initState, action) => {
  4. switch (action.type){
  5. case 'add':
  6. return {number: state.number + 1};
  7. case 'minus':
  8. return {number: state.number - 1};
  9. default:
  10. return state;
  11. }
  12. }
  13. let initState = {number: 10};
  14. let store = createStore(reducer, initState);
  15. + // 创建 action
  16. + const add = () => ({type: 'add'});
  17. + const minus = () => ({type: 'minus'});
  18. + // 创建一个 actionCreator 对象
  19. + const actions = {add, minus};
  20. + // 绑定 actionCreator
  21. + // 绑定之后 bindActions.add 就变成了 () => store.dispatch({type: 'add'})
  22. + const bindActions = bindActionCreators(actions, store.dispatch);
  23. class Counter1 extends React.Component{
  24. state={number: store.getState().number};
  25. componentDidMount(){
  26. this.unsubscribe = store.subscribe(() => {
  27. this.setState({number: store.getState().number});
  28. })
  29. }
  30. componentWillUnmount(){
  31. this.unsubscribe();
  32. }
  33. render (){
  34. return (
  35. <div>
  36. <p>{this.state.number}</p>
  37. + <button onClick={bindActions.add}>add</button>
  38. + <button onClick={bindActions.minus}>minus</button>
  39. </div>
  40. )
  41. }
  42. }
  43. export default Counter1;

combineReducers

https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts

  • redux应用中,reducer、store、state都只有一个,都是单例的。
  • 如果应用比较庞大的话,肯定要写很多 reducer,然后再合并成一个 reducer。

rwe.gif

src/index.js

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import Counter1 from './components/Counter1'
  4. import Counter2 from './components/Counter2'
  5. ReactDOM.render(
  6. <div>
  7. <Counter1 />
  8. <Counter2 />
  9. </div>,
  10. document.getElementById('root')
  11. );

src/store/index.js 仓库

  1. import {createStore} from 'redux';
  2. import rootReducer from './reducers';
  3. const store = createStore(rootReducer); //rootReducer 就是根reducer
  4. export default store;

src/store/action-types 动作类型

  1. export const ADD1 = 'ADD1';
  2. export const MINUS1 = 'MINUS1';
  3. export const ADD2 = 'ADD2';
  4. export const MINUS2 = 'MINUS2';
  5. export const ALLMINUS = 'ALLMINUS';

src/store/reduces 合并所有的分reducer

src/store/reducers/index.js
  1. import {combineReducers} from 'redux';
  2. import counter1 from './counter1';
  3. import counter2 from './counter2';
  4. // 合并所有的分 reducer
  5. let reducers = {counter1, counter2};
  6. let rootReducer = combineReducers(reducers);
  7. export default rootReducer;

src/store/reducers/counter1.js
  1. import * as types from '../action-types.js';
  2. // 这是 Counter1 组件对应的分 reducer,它有自己独立的状态
  3. const counter1 = (state={number:5}, action) => {
  4. switch (action.type){
  5. case types.ADD1:
  6. return {number: state.number + 1};
  7. case types.MINUS1:
  8. return {number: state.number - 1};
  9. case types.ALLMINUS:
  10. return {number: state.number - 1};
  11. default:
  12. return state;
  13. }
  14. }
  15. export default counter1;

src/store/reducers/counter2.js
  1. import * as types from '../action-types.js';
  2. // 这是 Counter2 组件对应的分 reducer,它有自己独立的状态
  3. const counter2 = (state={number:10}, action) => {
  4. switch (action.type){
  5. case types.ADD2:
  6. return {number: state.number + 1};
  7. case types.MINUS2:
  8. return {number: state.number - 1};
  9. case types.ALLMINUS:
  10. return {number: state.number - 1};
  11. default:
  12. return state;
  13. }
  14. }
  15. export default counter2;

src/store/actions 动作对象

src/store/actions/counter1.js
  1. import * as types from '../action-types';
  2. const actions = {
  3. add1: () => ({type: types.ADD1}),
  4. minus1: () => ({type: types.MINUS1}),
  5. }
  6. export default actions;

src/store/actions/counter2.js
  1. import * as types from '../action-types';
  2. const actions = {
  3. add2: () => ({type: types.ADD2}),
  4. minus2: () => ({type: types.MINUS2}),
  5. allMinus: () => ({type: types.ALLMINUS}),
  6. }
  7. export default actions;

src/components

可以看出组件里还是有很多重复的逻辑处理,这就需要用 react-redux 的connect 高阶组件去优化处理。

src/components/Counter1.js
  1. import React from 'react';
  2. import {bindActionCreators} from 'redux';
  3. + import store from '../store';
  4. + import actions from '../store/actions/counter1';
  5. const bindActions = bindActionCreators(actions, store.dispatch);
  6. class Counter1 extends React.Component{
  7. + state={number: store.getState().counter1.number};
  8. componentDidMount(){
  9. this.unsubscribe = store.subscribe(() => {
  10. + this.setState({number: store.getState().counter1.number});
  11. })
  12. }
  13. componentWillUnmount(){
  14. this.unsubscribe();
  15. }
  16. render (){
  17. return (
  18. <div>
  19. <p>{this.state.number}</p>
  20. + <button onClick={bindActions.add1}>add1</button>
  21. + <button onClick={bindActions.minus1}>minus1</button>
  22. </div>
  23. )
  24. }
  25. }
  26. export default Counter1;

src/components/Counter2.js
  1. import React from 'react';
  2. import {bindActionCreators} from 'redux';
  3. + import store from '../store';
  4. + import actions from '../store/actions/counter2';
  5. const bindActions = bindActionCreators(actions, store.dispatch);
  6. class Counter2 extends React.Component{
  7. + state={number: store.getState().counter2.number};
  8. componentDidMount(){
  9. this.unsubscribe = store.subscribe(() => {
  10. + this.setState({number: store.getState().counter2.number});
  11. })
  12. }
  13. componentWillUnmount(){
  14. this.unsubscribe();
  15. }
  16. render (){
  17. return (
  18. <div>
  19. <p>{this.state.number}</p>
  20. + <button onClick={bindActions.add2}>add2</button>
  21. + <button onClick={bindActions.minus2}>minus2</button>
  22. + <button onClick={bindActions.allMinus}>allMinus</button>
  23. </div>
  24. )
  25. }
  26. }
  27. 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( , document.getElementById(‘root’) );

  1. <a name="qHSeZ"></a>
  2. ## connect
  3. - [https://github.com/reduxjs/react-redux/tree/master/src/connect](https://github.com/reduxjs/react-redux/tree/master/src/connect)
  4. - 负责把仓库和组件进行关联
  5. <a name="dklzG"></a>
  6. ##### src/components/Counter1.js
  7. ```jsx
  8. import React from 'react';
  9. import {connect} from 'react-redux';
  10. import actions from '../store/actions/counter1';
  11. import * as types from '../store/action-types';
  12. class Counter1 extends React.Component{
  13. render (){
  14. let ps = this.props;
  15. return (
  16. <div>
  17. <p>{ps.number}</p>
  18. <button onClick={ps.add1}>add1</button>
  19. <button onClick={ps.minus1}>minus1</button>
  20. </div>
  21. )
  22. }
  23. }
  24. // 语法:connect(mapStateToProps, mapDispatchToProps, ...)
  25. // 第一个参数 mapStateToProps:状态映射函数。
  26. // 接收仓库的总状态,返回指定的分状态, connect再把函数返回值映射为组件属性
  27. function mapStateToProps(state){
  28. return state.counter1;
  29. }
  30. // 第二个参数 mapDispatchToProps:主要把经过 bingActionCreators 绑定后的 actions对象 映射为属性。
  31. // 大致有7中写法
  32. // 第1种,传函数。接收 store.dispatch 为参数,返回绑定后的 actions对象
  33. function mapDispatchToProps(dispatch){
  34. return {
  35. add1: () => dispatch({type: types.ADD1}),
  36. minus1: () => dispatch({type: types.MINUS1}),
  37. }
  38. }
  39. export default connect(mapStateToProps, mapDispatchToProps)(Counter1);
  40. // 第2种:直接传未经过绑定的 actions对象
  41. // export default connect(mapStateToProps, actions)(Counter1);

src/components/Counter2.js
  1. import React from 'react';
  2. import {connect} from 'react-redux';
  3. import actions from '../store/actions/counter2';
  4. class Counter2 extends React.Component{
  5. render (){
  6. let ps = this.props;
  7. return (
  8. <div>
  9. <p>{ps.number}</p>
  10. <button onClick={ps.add2}>add2</button>
  11. <button onClick={ps.minus2}>minus2</button>
  12. <button onClick={ps.allMinus}>allMinus</button>
  13. </div>
  14. )
  15. }
  16. }
  17. function mapStateToProps({counter2}){
  18. return counter2;
  19. }
  20. export default connect(mapStateToProps, actions)(Counter2);

hooks (useDispatch、useSelector、useState)

新版的react-redux为了更好的用于函数组件,提供了一些 hooks。

src/components/Counter1.js
  1. import React from 'react';
  2. import {useDispatch, useSelector, useStore} from 'react-redux';
  3. import * as types from '../store/action-types';
  4. function Counter1(){
  5. let dispatch = useDispatch(); //store.dispatch
  6. let state = useSelector(state => state.counter1); //获取状态
  7. let store = useStore(); //获取仓库 {dispatch, getState, subscribe}
  8. return (
  9. <div>
  10. <p>{state.number}</p>
  11. <button onClick={() => dispatch({type: types.ADD1})}>add1</button>
  12. <button onClick={() => dispatch({type: types.MINUS1})}>minus1</button>
  13. </div>
  14. )
  15. }
  16. export default Counter1;

applyMiddleware(redux中间件)

简述

  • 如果没有中间件的运用,redux的工作流程就是 action -> reducer ,这就相当于是同步操作,由dispatch触发action后,直接去reducer执行相应的动作。
  • 但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决问题。比如有一个这样的需求:点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时就需要引入中间件改变redux同步执行的流程,形成异步流程来实现所要的逻辑,有了中间件,redux的工作流程就变成:action -> middlewares -> reducer,点击按钮就相当于dispatch触发action,接下去获取服务器数据middlewares的执行,当middlewares成功获取到服务器数据就去触发reducer对应的动作,更新需要渲染视图的数据。
  • 中间件的机制可以让我们改变数据流,实现异步action,action过滤,日志输出,异常报告等功能。

Snipaste_2021-05-24_22-56-01.png
react-redux-flow.jpg

日志中间件

  • 实现一个日志中间件,在更改状态时,打印前后的状态。
  • 直接通过改写 dispatch 方法,实现在更改状态时,打印前后的状态。
  • 但这种方案并不好

实现日志

src/index.js

  1. import store from './store';
  2. store.subscribe(() => console.log('state =>', store.getState()));
  3. store.dispatch({type: 'ADD1'});

src/store/index.js

  1. import {createStore} from 'redux';
  2. import rootReducer from './reducers';
  3. const store = createStore(rootReducer);
  4. // 先获取原生的dispatch方法
  5. let dispatch = store.dispatch;
  6. // 再改写 dispatch 方法,实现在更改状态时,打印前后的状态
  7. store.dispatch = function(action){
  8. console.log('prev state', store.getState());
  9. dispatch(action);
  10. console.log('next state', store.getState());
  11. return action;
  12. }
  13. export default store;

image.png

实现异步
  1. import {createStore} from 'redux';
  2. import rootReducer from './reducers';
  3. const store = createStore(rootReducer);
  4. let dispatch = store.dispatch;
  5. store.dispatch = function(action){
  6. setTimeout(() => {
  7. dispatch(action);
  8. }, 3000);
  9. return action;
  10. }
  11. export default store;

image.png

单个日志中间件

  • 实现一个真正的日志中间件
    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) // }));

  1. <a name="nxzlX"></a>
  2. ##### src/store/index.js
  3. ```jsx
  4. import {createStore} from 'redux';
  5. import rootReducer from './reducers';
  6. function applyMiddleware(middleware){
  7. return function(createStore){
  8. return function(reducer){
  9. let store = createStore(reducer); //先创建原生的store
  10. let dispatch; //指向改造后的dispatch方法
  11. let middlewareAPI = {
  12. getState: store.getState,
  13. dispatch: action => dispatch(action),
  14. }
  15. dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法
  16. return {...store, dispatch};
  17. }
  18. }
  19. }
  20. // 日志中间件的真正实现(只要是中间件,格式都是定死的)
  21. function logger(store){
  22. return function(next){
  23. return function(action){
  24. console.log('prev state', store.getState());
  25. next(action);
  26. console.log('next state', store.getState());
  27. }
  28. }
  29. }
  30. const store = applyMiddleware(logger)(createStore)(rootReducer);
  31. export default store;

单个支持派发函数、promise的中间件

支持函数
  1. // 原生的 store.dispatch 只支持传递普通对象,不能接受函数,也不能接收promise。
  2. // 为了要让其支持,添加 支持函数的中间件
  3. function thunk(store){
  4. return function(next){
  5. return function(action){
  6. // 如果派发的action是一个函数,就让它执行
  7. if (typeof action === 'function'){
  8. // 如果这里第一个参数传递next(即原生的store.dispatch方法),
  9. // 那么函数内 dispatch 如果继续派发函数,那就报错了。
  10. return action(store.dispatch, store.getState);
  11. }
  12. // 如果不是函数,则意味着不需要自己处理。直接调用下一个dispatch方法即可。
  13. return next(action);
  14. }
  15. }
  16. }
  17. const store = applyMiddleware(thunk)(createStore)(rootReducer);

支持promise
  1. // 添加支持promise的中间件
  2. function promise(store){
  3. return function(next){
  4. return function(action){
  5. if (typeof action.then === 'function'){
  6. return action.then(newAction => store.dispatch(newAction));
  7. }
  8. return next(action);
  9. }
  10. }
  11. }
  12. const store = applyMiddleware(promise)(createStore)(rootReducer);

级联中间件

  • 上面都是单一的中间件,但是真正项目里假如有很多中间件,怎么办呢?
  • 在真实的项目中,中间件每个逻辑都是单独编写的,但是可以向 applyMiddleware传递多个中间件
  • const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);

middleware.png

  1. function applyMiddleware(...middlewares){
  2. return function(createStore){
  3. return function(reducer){
  4. let store = createStore(reducer); //先创建原生的store
  5. let dispatch; //指向改造后的dispatch方法
  6. let middlewareAPI = {
  7. getState: store.getState,
  8. dispatch: action => dispatch(action),
  9. }
  10. // dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法
  11. let chain = middlewares.map(item => item(middlewareAPI));
  12. let [promise, thunk, logger] = chain;
  13. dispatch = promise(thunk(logger(store.dispatch)));
  14. return {...store, dispatch};
  15. }
  16. }
  17. }
  18. const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);

看起来,是不是有点洋葱模型的感觉了,这里就引出来了 compose 管道。

compose、applyMiddleware 的具体实现见下一节 redux-源码篇

示例

src/store/index.js
  1. import {createStore, applyMiddleware} from 'redux';
  2. import reduxLogger from 'redux-logger';
  3. import reduxThunk from 'redux-thunk';
  4. import reduxPromise from 'redux-promise';
  5. import rootReducer from './reducers';
  6. const store = applyMiddleware(
  7. reduxPromise,
  8. reduxThunk,
  9. reduxLogger
  10. )(createStore)(rootReducer);
  11. export default store;

src/components/Couter1.js
  1. import React from 'react';
  2. import {useDispatch, useSelector, useStore} from 'react-redux';
  3. import * as types from '../store/action-types';
  4. function Counter1(){
  5. let dispatch = useDispatch(); //store.dispatch
  6. let state = useSelector(state => state.counter1); //获取状态
  7. let store = useStore(); //获取仓库 {dispatch, getState, subscribe}
  8. return (
  9. <div>
  10. <p>{state.number}</p>
  11. {/* 派发参数为 普通对象 */}
  12. <button onClick={() => dispatch({type: types.ADD1})}>add1</button>
  13. <button onClick={() => dispatch({type: types.MINUS1})}>minus1</button>
  14. {/* 派发参数为 函数 */}
  15. <button onClick={() => dispatch((resDispatch, resGetState) => {
  16. setTimeout(() => {
  17. resDispatch({type: types.ADD1});
  18. }, 2000)
  19. })}>function +</button>
  20. {/* 派发参数为 Promise */}
  21. <button onClick={() => dispatch(new Promise((resolve) => {
  22. setTimeout(() => {
  23. resolve({type: types.MINUS1});
  24. }, 2000)
  25. }))}>Promise -</button>
  26. </div>
  27. )
  28. }
  29. export default Counter1;