Redux使用和剖析

目录

 

一、JavaScript纯函数

二、为什么需要redux(传送门)

三、Redux的核心理念 - Store

四、Redux的核心理念 - action

五、Redux的核心理念 - reducer

六、Redux的三大原则

七、Redux测试项目搭建

1、安装redux:

2、redux项目创建:

八、 Redux的使用过程

九、Redux结构划分

十、Redux使用流程

十一、Redux官方图

十二、redux融入react代码

十三、高阶函数

十四、高阶组件

十五、高阶组建的定义

十六、高阶组建应用---- props的增强

 十七、高阶组建应用---- 渲染判断鉴权

十八、高阶组建应用---- 生命周期劫持

十九、自定义connect函数

二十、使用redux的connect:

二十二、hoist-non-react-statics使用

二十三、组件中异步操作

二十四、redux-thunk中间件

1、如何在redux中进行异步网络请求呢?

2、如何使用redux-thunk

二十五、redux-devtools

二十六、generator

二十七、redux-saga的使用

二十八、中间件

1、打印日志需求 (案例使用第七点搭建的learn-redux项目的基础上进行修改)

2、合并中间件

二十九、Reducer

1、Reducer代码拆分

2、Reducer文件拆分

3、combineReducers函数

4、combineReducers源码解读

​​


一、JavaScript纯函数

函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;

在React中,纯函数的概念非常重要,在接下来我们学习的Redux中也非常重要,所以先了解一下纯函数。

 
  1. 纯函数的维基百科定义:

    在程序设计中,若一个函数符合一下条件,那么这个函数被称为纯函数:

    1)此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的

    外部输出无关。

    2)该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

    3)当然上面的定义会过于的晦涩,所以我简单总结一下:

    4) 确定的输入,一定会产生确定的输出;

    5) 函数在执行过程中,不能产生副作用;
  2. 纯函数分析

    案例1:纯函数
     
    function sum(num1 ,num2){
        return num1 + num2;
    }
    它的输出是依赖我们的输入内容,并且中间没有产生任何副作用;

    案例2:非纯函数
     
    let foo = 5;
    function addNum(num){
        return num + foo;
    }
    函数依赖一个外部的变量,变量发生改变时,会影响:确定的输入,产生确定的输出;

    能否改进成纯函数呢? const foo = 5; 即可
  3. React中的纯函数

    1)为什么纯函数在函数式编程中非常重要呢?

    2)因为你可以安心的写和安心的用;

    3)你在写的时候保证了函数的纯度,只是关心实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;

    4)你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

    5)React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

    6)传送门



    7)在redux中,reducer被要求是一个纯函数。

二、为什么需要redux(传送门

  1. JavaScript开发的应用程序,已经变得越来越复杂了:

    JavaScript需要管理的状态越来越多,越来越复杂;

    这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中, 是否显示加载动效,当前分页;
     
  2. 管理不断变化的state是非常困难的:

    状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;

    当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
     
  3. React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

    无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;

    React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;
     
    UI = render(state)

     
  4. Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
  5. Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

三、Redux的核心理念 - Store

  1. 比如我们有一个朋友列表需要管理:

    如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;

    比如页面的某处通过products.push的方式增加了一条数据;

    比如另一个页面通过products[0].age = 25修改了一条数据;
     
  2. 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;

四、Redux的核心理念 - action

 

 

  1. Redux要求我们通过action来更新数据:

    所有数据的变化,必须通过派发(dispatch)action来更新;

    action是一个普通的JavaScript对象,用来描述这次更新的type和content;
     
  2. 比如下面就是几个更新friends的action:

    强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;

    当然,目前我们的action是固定的对象,真实应用中,我们会通过函数来定义,返回一个action;

 

五、Redux的核心理念 - reducer

  • reducer是一个纯函数;
  • reducer做的事情就是将传入的state和action结合起来生成一个新的state;

六、Redux的三大原则

  1. 单一数据源
     
    整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:

    Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;

    单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
  2. State是只读的

    唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:

    这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;

    这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
  3. 使用纯函数来执行修改

    通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:

    随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;

    但是所有的reducer都应该是纯函数,不能产生任何的副作用

七、Redux测试项目搭建

1、安装redux:

npm install redux --save


 

yarn add redux

2、redux项目创建:

  1. 创建项目文件夹:learn-redux
  2. 执行初始化操作:
     
    yarn init
    yarn init -y

     

  3. 安装redux
     
    yarn add redux
  4. 创建index.js文件

八、 Redux的使用过程

  1. 创建一个对象,作为我们要保存的状态:
  2. 创建Store来存储这个state

    创建store时必须创建reducer;

    我们可以通过 store.getState 来获取当前的state
  3. 通过action来修改state

    通过dispatch来派发action;

    通常action中都会有type属性,也可以携带其他的数据;
  4. 修改reducer中的处理代码

    这里一定要记住,reducer是一个纯函数,不需要直接修改state;

    后面我会讲到直接修改state带来的问题;
  5. 可以在派发action之前,监听store的变化:
     
    // reducer store reducer
    
    var redux = require('redux');
    const initialState = {
      counter: 0,
    };
    
    //reducer
    function reducer(state = initialState, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { ...state, counter: state.counter + 1 };
        case 'DECREMENT':
          return { ...state, counter: state.counter - 1 };
        case 'ADD_NUMBER':
          return { ...state, counter: state.counter + action.num };
        case 'SUB_NUMBER':
          return { ...state, counter: state.counter - action.num };
        default:
          return state;
      }
    }
    
    //store
    const store = redux.createStore(reducer);
    
    //actions
    const action1 = { type: 'INCREMENT' };
    const action2 = { type: 'DECREMENT' };
    const action3 = { type: 'ADD_NUMBER' ,num:5 };
    const action4 = { type: 'SUB_NUMBER' ,num:20};
    
    //派发action
    store.dispatch(action1);
    store.dispatch(action2);
    store.dispatch(action3);
    store.dispatch(action4);
    
  6. 为了能验证派发之后state发生了改变,我可以订阅store的修改
     
    // 订阅store的修改
    store.subscribe(() => {
      console.log('counter:' ,store.getState().counter);
    });

    这里需要注意的是:该订阅的代码必须放在action派发之前,因为在执行node命令时,代码是从上往下一次执行,完整代码:

    // reducer store reducer
    
    var redux = require('redux');
    const initialState = {
      counter: 0,
    };
    
    //reducer
    function reducer(state = initialState, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { ...state, counter: state.counter + 1 };
        case 'DECREMENT':
          return { ...state, counter: state.counter - 1 };
        case 'ADD_NUMBER':
          return { ...state, counter: state.counter + action.num };
        case 'SUB_NUMBER':
          return { ...state, counter: state.counter - action.num };
        default:
          return state;
      }
    }
    
    //store
    const store = redux.createStore(reducer);
    
    //actions
    const action1 = { type: 'INCREMENT' };
    const action2 = { type: 'DECREMENT' };
    const action3 = { type: 'ADD_NUMBER' ,num:5 };
    const action4 = { type: 'SUB_NUMBER' ,num:20};
    
    // 订阅store的修改(该订阅的代码必须放在action派发之前,因为在执行node命令时,代码是从上往下一次执行)
    store.subscribe(() => {
      console.log('counter:' ,store.getState().counter);
    });
    
    //派发action
    store.dispatch(action1);
    store.dispatch(action2);
    store.dispatch(action3);
    store.dispatch(action4);
    

     

  7. 然后终端执行node命令:
     

    node index.js

    控制台打印结果:

    learn-redux % node index.js
    counter: 1
    counter: 0
    counter: 5

     

九、Redux结构划分

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。

  1. 接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
     

    创建store/index.js文件:

    import redux from 'redux';
    import reducer from './reducer.js';
    
    const store = redux.createStore(reducer);
    
    export default store;
    

    创建store/reducer.js文件:

    
    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT
    } from './constants.js'
    const defaultState = {
      counter: 0
    }
    
    function reducer(state = defaultState ,action){
      switch (action.type) {
        case INCREMENT:
          return { ...state, counter: state.counter + 1 };
        case DECREMENT:
          return { ...state, counter: state.counter - 1 };
        case ADD_NUMBER:
          return { ...state, counter: state.counter + action.num };
        case SUB_NUMBER:
          return { ...state, counter: state.counter - action.num };
        default:
          return state;
      }
    }
    
    export default reducer;

    创建store/actions.js文件:

    
    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT
    } from './constants.js'
    
    export const addAction = num => ({
      type: ADD_NUMBER,
      num,
    });
    
    export const subAction = num =>({
      type:SUB_NUMBER,
      num
    })
    
    export const decrementAction = _ =>({ type:DECREMENT })
    
    export const incrementAction = _ =>({ type:INCREMENT })

    创建store/constants.js文件:

    export const ADD_NUMBER = 'ADD_NUMBER' 
    export const SUB_NUMBER = 'SUB_NUMBER' 
    export const INCREMENT = 'INCREMENT' 
    export const DECREMENT = 'DECREMENT' 

    终端运行:
     

    yarn start

    输出结果:

    learn-redux2 % yarn start
    yarn run v1.22.4
    warning ../../../../package.json: No license field
    $ node --experimental-modules index.js
    (node:38435) ExperimentalWarning: The ESM module loader is experimental.
    { counter: 5 }
    { counter: -5 }
    { counter: -6 }
    { counter: -5 }
    ✨  Done in 0.23s.

     

  2.  注意:node中对ES6模块化的支持
     

    目前我使用的node版本是v13.7.0,从node v13.2.0开始,node才对ES6模块化提供了支持:

    node v13.2.0之前,需要进行如下操作:

    在package.json中添加属性: "type": "module";

    在执行命令中添加如下选项:node --experimental-modules src/index.js;

    node v13.2.0之后,只需要进行如下操作:

    在package.json中添加属性: "type": "module";

    注意:导入文件时,需要跟上.js后缀名;

    {
      "name": "learn-redux2",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "type": "module",
      "scripts": {
        "start": "node --experimental-modules index.js"
      },
      "dependencies": {
        "redux": "^4.0.5"
      }
    }
    

     

十、Redux使用流程

redux在实际开发中的流程:

 

十一、Redux官方图

十二、redux融入react代码

目前redux在react中使用是最多的,所以我们需要将之前编写的redux代码,融入到react当中去。

  1. 这里我创建了两个组件:

    Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;

    About组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;
     
  2. 核心代码主要是两个:

    在 componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;

    在发生点击事件时,调用store的dispatch来派发对应的action;
     
  3. 将第九条中的store文件夹拖入到新创建的react项目中:这里需要注意修改的是redux的引入方式有点变化:


     
  4. app.js文件:
     
    import React from 'react';
    import About from './pages/about';
    import Home from './pages/home';
    
    function App() {
      return (
        <div className="App">
          <Home/>
          <About/>
        </div>
      );
    }
    
    export default App;
    
  5. home.js文件:
     
    import React, { PureComponent } from 'react'
    import store from '../store';
    import {incrementAction ,addAction} from '../store/actions';
    
    
    export default class Home extends PureComponent {
      constructor(props){
        super(props);
        this.state = {
          counter: store.getState().counter
        }
      }
    
      componentDidMount() {
        this.unsubscribue = store.subscribe(() => {
          this.setState({
            counter: store.getState().counter
          })
        })
      }
    
      componentWillUnmount() {
        this.unsubscribue();
      }
    
      increment = ()=> {
        store.dispatch(incrementAction());
      }
    
      addNumber = (num)=>{
        store.dispatch(addAction(num));
      }
    
      render() {
        return (
          <div>
            <h1>home</h1>
            <h2>当前计数:{this.state.counter}</h2>
            <button onClick = {()=>this.increment()}>+1</button>
            <button onClick = {()=>this.addNumber(5)}>+5</button>
          </div>
        )
      }
    }
    
  6. about.js文件:
     
    import React, { PureComponent } from 'react';
    import store from '../store';
    import { decrementAction, subAction } from '../store/actions';
    export default class About extends PureComponent {
      constructor(props) {
        super(props);
        this.state = {
          counter: store.getState().counter,
        };
      }
    
      componentDidMount() {
        this.unsubscribue = store.subscribe(() => {
          this.setState({
            counter: store.getState().counter,
          });
        });
      }
    
      componentWillUnmount() {
        this.unsubscribue();
      }
    
      decrement = () => {
        store.dispatch(decrementAction());
      };
    
      subNumber = (num) => {
        store.dispatch(subAction(num));
      };
    
      render() {
        return (
          <div>
            <hr />
            <h1>about</h1>
            <h2>当前计数:{this.state.counter}</h2>
            <button onClick={(e) => this.decrement()}>-1</button>
            <button onClick={(e) => this.subNumber(5)}>-5</button>
          </div>
        );
      }
    }
    
  7. 运行效果:

十三、高阶函数

  1. 高阶函数的维基百科定义:至少满足以下条件之一:

     接受一个或多个函数作为输入;

     输出一个函数;
     
  2. JavaScript中比较常见的filter、map、reduce都是高阶函数。

十四、高阶组件

  1. 什么是高阶组件呢?

    高阶组件的英文是 Higher-Order Components,简称为 HOC;

     官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
  2. 我们可以进行如下的解析:

    首先, 高阶组件 本身不是一个组件,而是一个函数;

    其次,这个函数的参数是一个组件,返回值也是一个组件;

十五、高阶组建的定义

案例:

  • import React, { PureComponent } from 'react';
    
    class App extends PureComponent {
      render() {
        return <div>aaa</div>;
      }
    }
    
    function enhanceComponent(WrappedComponent) {
      return class NewComponent extends PureComponent {
        render() {
          return <WrappedComponent />;
        }
      };
    }
    
    const EnhanceComponent = enhanceComponent(App);
    
    export default EnhanceComponent;

 

  1.  高阶组件的调用过程类似于这样:
     
    const EnhanceComponent = enhanceComponent(App);
  2. 高阶组建的编写过程类似于这样:
     
    function enhanceComponent(WrappedComponent) {
      return class NewComponent extends PureComponent {
        render() {
          return <WrappedComponent />;
        }
      };
    }
  3. 组件的名称问题:

     在ES6中,类表达式中类名是可以省略的
     
    function enhanceComponent(WrappedComponent) {
      return class extends PureComponent {
        render() {
          return <WrappedComponent />;
        }
      };
    }
     运行结果:省略之后默认显示它所继承的父组建名称

  4. 组件的名称都可以通过displayName来修改;
     
    App.displayName = 'AAA';
    function enhanceComponent(WrappedComponent) {
      const NewComponent = class extends PureComponent {
        render() {
          return <WrappedComponent />;
        }
      };
    
      NewComponent.displayName = 'BBB';
    
      return NewComponent;
    }
    运行结果:

  5. 高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;
  6.  高阶组件在一些React第三方库中非常常见:

    比如redux中的connect;

    比如react-router中的withRouter;

十六、高阶组建应用---- props的增强

案例:

import React, { PureComponent } from 'react'


class About extends PureComponent {
  render() {
    return <h2>About:{`名字:${this.props.name} 区域:${this.props.region}`}</h2>
  }
}

class Home extends PureComponent {
  render() {
    return <h2>About:{`名字:${this.props.name} 区域:${this.props.region}`}</h2>
  }
}

function enhanceRegionProps(WrappedComponent){
  return props => {
    return <WrappedComponent {...props} region="非洲"/>
  }
}

const EnhanceAbout = enhanceRegionProps(About);
const EnhanceHome = enhanceRegionProps(Home);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <EnhanceAbout name="张三"/>
        <EnhanceHome name="里斯"/>
      </div>
    )
  }
}
  1. 不修改原有代码的情况下,添加新的props
     
    function enhanceRegionProps(WrappedComponent ,otherProps){
      return props => <WrappedComponent {...props} {...otherProps}/>
    }
  2. 利用高阶组件来共享Context

    默认代码:

     
    import React, { PureComponent, createContext } from 'react'
    
    
    //创建context
    const UserContext = createContext({
      region:"重庆",
      nickname:'默认昵称'
    })
    
    class Home extends PureComponent{
      render(){
        return(
          <UserContext.Consumer>
            {
              user => {
                return <h2>Home: {`昵称: ${user.nickname} 区域: ${user.region}`}</h2>
              }
            }
          </UserContext.Consumer>
        )
      }
    }
    
    class About extends PureComponent {
      render() {
        return (
          <UserContext.Consumer>
            {
              user => {
                return <h2>About: {`昵称: ${user.nickname} 区域: ${user.region}`}</h2>
              } 
            }
          </UserContext.Consumer>
        )
      }
    }
    
    export default class App extends PureComponent {
      render() {
        return (
          <div>
            <UserContext.Provider value={{nickname: "fuyun", region: "陕西"}}>
              <Home/>
              <About/>
            </UserContext.Provider>
          </div>
        )
      }
    }
    
    利用高阶组建修改后代码:

     
    import React, { PureComponent, createContext } from 'react'
    
    //定义高阶组建
    function withUser(WrappedComponent){
      return props => {
        return (
          <UserContext.Consumer>
            {
              user => {
                return <WrappedComponent {...props} {...user}/>
              }
            }
          </UserContext.Consumer>
        )
      }
    }
    
    //创建context
    const UserContext = createContext({
      region:"重庆",
      nickname:'默认昵称'
    })
    
    class Home extends PureComponent{
      render(){
        return <h2>Home: {`昵称: ${this.props.nickname} 区域: ${this.props.region}`}</h2>
      }
    }
    
    class About extends PureComponent {
      render() {
        return <h2>About: {`昵称: ${this.props.nickname} 区域: ${this.props.region}`}</h2>
      }
    }
    
    const UserHome = withUser(Home);
    const UserAbout = withUser(About);
    
    export default class App extends PureComponent {
      render() {
        return (
          <div>
            <UserContext.Provider value={{nickname: "fuyun", region: "陕西"}}>
              <UserHome/>
              <UserAbout/>
            </UserContext.Provider>
          </div>
        )
      }
    }
    
    分析:

    修改之前的类组建




    修改之后的类组建:



    将公共的部分抽离到高阶组建:

     

 十七、高阶组建应用---- 渲染判断鉴权

 

  1. 开发中遇到这样的场景:

    某些页面是必须用户登录成功才能进行进入;

    如果用户没有登录成功,那么直接跳转到登录页面;
     
  2. 这个时候,我们就可以使用高阶组件来完成鉴权操作:
import React, { PureComponent } from 'react'


function withAuth(WrappedComponent){
  const NewCpn = props => {
    const {isLogin} = props;
    if (isLogin){
      return <WrappedComponent {...props}/>
    }else{
      return <LoginPage/>
    }
  }

  NewCpn.displayName = 'AuthCpn';

  return NewCpn;
}

class LoginPage extends PureComponent{
  render(){
    return <h2>LoginPage</h2>
  }
}

class CartPage extends PureComponent{
  render(){
    return <h2>CartPage</h2>
  }
}

const AuthCartPage = withAuth(CartPage);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <AuthCartPage isLogin={true}/>
      </div>
    )
  }
}

高阶组建:开发中是否登录的状态一般存储在本地直接获取登录状态,不需要外面传入登录状态

function withAuth(WrappedComponent){
  const NewCpn = props => {
    //开发中是否登录的状态一般存储在本地直接获取登录状态,不需要外面传入登录状态
    const {isLogin} = global;
    if (isLogin){
      return <WrappedComponent {...props}/>
    }else{
      return <LoginPage/>
    }
  }

  NewCpn.displayName = 'AuthCpn';

  return NewCpn;
}


十八、高阶组建应用---- 生命周期劫持

  我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

 默认代码:

import React, { PureComponent } from 'react';

class Home extends PureComponent {

  // 即将渲染获取一个时间 beginTime
  UNSAFE_componentWillMount() {
    this.beginTime = Date.now();
  }

  // 渲染完成再获取一个时间 endTime
  componentDidMount() {
    this.endTime = Date.now();
    const interval = this.endTime - this.beginTime;
    console.log(`Home渲染时间: ${interval}`)
  }

  render() {
    return <h2>Home</h2>
  }
}


class About extends PureComponent {
  // 即将渲染获取一个时间 beginTime
  UNSAFE_componentWillMount() {
    this.beginTime = Date.now();
  }

  // 渲染完成再获取一个时间 endTime
  componentDidMount() {
    this.endTime = Date.now();
    const interval = this.endTime - this.beginTime;
    console.log(`About渲染时间: ${interval}`)
  }

  render() {
    return <h2>About</h2>
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home />
        <About />
      </div>
    )
  }
}

修改之后代码: 

import React, { PureComponent } from 'react';

function withRenderTime(WrappedComponent) {
  return class extends PureComponent {
    // 即将渲染获取一个时间 beginTime
    UNSAFE_componentWillMount() {
      this.beginTime = Date.now();
    }

    // 渲染完成再获取一个时间 endTime
    componentDidMount() {
      this.endTime = Date.now();
      const interval = this.endTime - this.beginTime;
      console.log(`${WrappedComponent.name}渲染时间: ${interval}`)
    }

    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About</h2>
  }
}

const TimeHome = withRenderTime(Home);
const TimeAbout = withRenderTime(About);

export default class App extends PureComponent {

  render() {
    return (
      <div>
        <TimeHome />
        <TimeAbout />
      </div>
    )
  }
}

 

十九、自定义connect函数

创建connect.js文件:

import React ,{ PureComponent } from 'react';
import store from '../store';

export function connect(mapStateToProps, mapDispatchToProps) {
  return function enhanceHOC(WrappedComponent) {
    return class extends PureComponent {
      constructor(props){
        super(props)
        this.state = {
          storeState: mapStateToProps(store.getState())
        }
      }
      componentDidMount(){
        this.unsubscribe = store.subscribe(()=> {
          this.setState({
            storeState: mapStateToProps(store.getState())
          })
        })
      }

      componentWillUnmount(){
        this.unsubscribe()
      }

      render() {
        return <WrappedComponent 
        {...this.props} 
        {...this.state.storeState} 
        {...mapDispatchToProps(store.dispatch)}/>;
      }
    };
  };
}

about.js文件:
 

import React from 'react';
import { decrementAction, subAction } from '../store/actions';

import {connect} from '../utils/connect'

const About = (props)=> {
    return (
      <div>
        <hr />
        <h1>about</h1>
        <h2>当前计数:{props.counter}</h2>
        <button onClick={(e) => props.decrement()}>-1</button>
        <button onClick={(e) => props.subNumber(5)}>-5</button>
      </div>
    );
}

const mapStateToProps = state => {
  return {
    counter: state.counter
  };
};
const mapDispatchToProps = dispatch => {
  return {
    decrement: function (){
      dispatch(decrementAction())
    },
    subNumber: function (num){
      dispatch(subAction(num))
    }
  };
};

export default connect(mapStateToProps , mapDispatchToProps)(About);

相比之前的about代码:

替换为映射:

connect的功能基本实现了,但是如果有一个致命缺陷,就是在connect.js文件中需要引入store:

import store from '../store';

作为一个工具文件,或者三方库,需要引入别人业务层的sotre,显然是不行的,我们需要使用context,将store由外面传入:

创建context.js文件:

import React from 'react'

const StoreContext = React.createContext();

export {
  StoreContext
}

继续改造connect.js文件:将store的地方使用context替换

import React ,{ PureComponent } from 'react';
import {StoreContext} from './context'

export function connect(mapStateToProps, mapDispatchToProps) {
  return function enhanceHOC(WrappedComponent) {
    class EnhanceComponet extends PureComponent {
      constructor(props ,context){
        super(props ,context)
        this.state = {
          storeState: mapStateToProps(context.getState())
        }
      }

      componentDidMount(){
        this.unsubscribe = this.context.subscribe(()=> {
          this.setState({
            storeState: mapStateToProps(this.context.getState())
          })
        })
      }

      componentWillUnmount(){
        this.unsubscribe()
      }

      render() {
        return <WrappedComponent 
        {...this.props} 
        {...this.state.storeState} 
        {...mapDispatchToProps(this.context.dispatch)}/>;
      }
    };
    EnhanceComponet.contextType = StoreContext;
    return EnhanceComponet;
  };
}

然后在外面使用创建的context传入store:

index.js文件:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {StoreContext} from './utils/context'
import store from './store'

ReactDOM.render(
  <StoreContext.Provider value = {store}>
    <App />
  </StoreContext.Provider>
  ,
  document.getElementById('root')
);

二十、使用redux的connect:

接着上面的代码,我们在项目中引入react-redux,终端输入:

yarn add react-redux

然后将Provider和connect替换为react-redux的就ok了:

二十一、react-redux源码解读

  1. Provider源码:

  2. connect源码:通过导出createConnect()函数调用,做一些参数的初始化操作,然后return一个connect函数

  3. connect函数作用:
  4. wrapWithConnect高阶组建的作用:给我们组建添加了一些属性,并创建了一个Connect组建,并对Connect组建和传入的WrappedComponent的属性进行一个合并然后返回:


    这里需要注意的是hoistStatics是引入的三方hoist-non-react-statics的函数:
     

二十二、hoist-non-react-statics使用


当使用高阶组建包装组建时,即原始的组建被容器组建包裹后,会导致新组建会丢失原始组建的所有静态方法,还是上面的例子:
我们给About组建添加一个测试方法:

这个时候我们在调用高阶组建的时候,就会丢失这里的test静态方法,除非进行一一拷贝给新组建:

这样做很显然你是需要知道原始组建的所有静态方法的,作为一个三方的库而已,显然不现实,这里就可以用到hoist-non-react-statics库,帮你自动拷贝所有非React的静态方法:

hoistStatics(EnhanceComponet ,WrappedComponent)

二十三、组件中异步操作

  1.  在上面的案例中,redux中保存的counter是一个本地定义的数据

    我们可以直接通过同步的操作来dispatch action,state就会被立即更新。

    但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。
  2. 网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

  3. 上面的流程有一个缺陷:

    我们必须将网络请求的异步代码放到组件的生命周期中来完成;

    事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;

二十四、redux-thunk中间件

网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
 

1、如何在redux中进行异步网络请求呢?

  1. redux中有引入了中间件(Middleware)的概念:

    这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;

    比如日志记录、调用异步接口、添加代码调试功能等等;
  2. 我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:

    官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;
  3. redux-thunk是如何做到让我们可以发送异步的请求呢?

    我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;

    redux-thunk可以让dispatch(action函数),action可以是一个函数
  4. 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;

    dispatch函数用于我们之后再次派发action;

    getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

2、如何使用redux-thunk

继续上面的案例

  1.  安装redux-thunk
    yarn add redux-thunk
  2. 在创建store时传入应用了middleware的enhance函数 

    通过applyMiddleware来结合多个Middleware, 返回一个enhancer;

    将enhancer作为第二个参数传入到createStore中;

    在store目录下的index.js文件中:
     
    import { createStore ,applyMiddleware } from 'redux';
    import reducer from './reducer.js';
    import thunkMiddleware from 'redux-thunk'
    
    const storeEnhancer = applyMiddleware(thunkMiddleware);
    const store = createStore(reducer ,storeEnhancer);
    
    export default store;

     

  3. 定义返回一个函数的action:

    注意:这里不是返回一个对象了,而是一个函数;

    该函数在dispatch之后会被执行;

    需要网络请求,引入axios,终端输入:
     
    yarn add axios
    // redux-thunk异步请求
    export const fetchDataAction = (dispatch, getState) => {
      axios({
        url: "http://localhost:3000/data",
      }).then(res => {
        const data = res.data;
        console.log(data);
        dispatch({
          type: FETCH_SERVICE_DATA,
          data
        });
      })
    };

     

  4. 为了能制造一个server环境,请求到json,我们搭建一个json-server服务,写一串json 传送门 然后跑起来:

    大概步骤:新建文件夹server ---->  cd server ---> 终端yarn init ---->新建db.json文件 ---> 配置package.json文件的start启动项






     
  5. 有了服务以后跑起来我们的案例就能看到log数据了,为了能8把数据展示到界面上,我们添加li标签

    完整代码:

    store/index.js文件:
     
    import { createStore ,applyMiddleware } from 'redux';
    import reducer from './reducer.js';
    import thunkMiddleware from 'redux-thunk'
    
    const storeEnhancer = applyMiddleware(thunkMiddleware);
    const store = createStore(reducer ,storeEnhancer);
    
    export default store;
    

    store/actions.js:
     

    import axios from 'axios';
    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT,
      FETCH_SERVICE_DATA
    } from './constants.js'
    
    
    export const addAction = num => ({
      type: ADD_NUMBER,
      num,
    });
    
    export const subAction = num =>({
      type:SUB_NUMBER,
      num
    })
    
    export const decrementAction = () =>({ type:DECREMENT })
    
    export const incrementAction = () =>({ type:INCREMENT })
    
    // redux-thunk异步请求
    export const fetchDataAction = (dispatch, getState) => {
      axios({
        url: "http://localhost:3000/data",
      }).then(res => {
        const data = res.data;
        console.log(data);
        dispatch({
          type: FETCH_SERVICE_DATA,
          data
        });
      })
    };
    
    

    store/constants.js
     

    export const ADD_NUMBER = 'ADD_NUMBER' 
    export const SUB_NUMBER = 'SUB_NUMBER' 
    export const INCREMENT = 'INCREMENT' 
    export const DECREMENT = 'DECREMENT' 
    export const FETCH_SERVICE_DATA = 'FETCH_SERVICE_DATA' 

    store/reducer.js
     

    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT,
      FETCH_SERVICE_DATA,
    } from './constants.js';
    const defaultState = {
      counter: 0,
    };
    
    function reducer(state = defaultState, action) {
      switch (action.type) {
        case INCREMENT:
          return { ...state, counter: state.counter + 1 };
        case DECREMENT:
          return { ...state, counter: state.counter - 1 };
        case ADD_NUMBER:
          return { ...state, counter: state.counter + action.num };
        case SUB_NUMBER:
          return { ...state, counter: state.counter - action.num };
        case FETCH_SERVICE_DATA:
          return { ...state, data: action.data };
        default:
          return state;
      }
    }
    
    export default reducer;
    

    home.js
     

    import React, { PureComponent } from 'react'
    import {incrementAction ,addAction ,fetchDataAction} from '../store/actions';
    import {connect} from 'react-redux';
    
    class Home extends PureComponent {
     
      componentDidMount() {
        this.props.fetchData();
      }
      render() {
        const {data = []} = this.props;
        return (
          <div>
            <h1>home</h1>
            <h2>当前计数:{this.props.counter}</h2>
            <button onClick = {()=>this.props.increment()}>+1</button>
            <button onClick = {()=>this.props.addNumber(5)}>+5</button>
            <br/>
            <ul>
              {
                data.map((item)=>{
                  return (
                  <li key={item.id}>{item.title}</li>
                  )
                })
              }
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = state => ({
      counter:state.counter,
      data:state.data
    })
    const mapDispatchToProps = dispatch => ({
      increment: ()=> {
        dispatch(incrementAction())
      },
      addNumber: (num)=> {
        dispatch(addAction(num))
      },
      fetchData: ()=> {
        dispatch(fetchDataAction)
      }
    })
    
    export default connect(mapStateToProps ,mapDispatchToProps)(Home)
    
    

    这里需要注意的是,由于引入了redux-thunk中间件后,home.js文件中dispatch的一定是个函数,而不是函数的调用:

    该dispatch传入一个函数fetchDataAction,redux-thunk内部会对fetchDataAction函数进行调用,因此一定不能写成函数调用

    运行看结果:

二十五、redux-devtools

  1. redux官网为我们提供了redux-devtools(传送门)的工具,利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
  2. 安装该工具需要两步:

    第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参GitHub)(传送门);

    第二步:在redux中继承devtools的中间件;

    store/index.js文件中

     
    import { createStore, applyMiddleware, compose } from 'redux';
    import reducer from './reducer.js';
    import thunkMiddleware from 'redux-thunk';
    
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
    
    const storeEnhancer = applyMiddleware(thunkMiddleware);
    const store = createStore(reducer, composeEnhancers(storeEnhancer));
    
    export default store;
    就可以调试了,当然如果需要用到trace功能,还需要添加:trace参数为true:
     
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace:true}) || compose;
    
    运行:

二十六、generator

saga中间件使用了ES6的generator语法,简单了解下generator的用法

我们按照如下步骤演示一下生成器的使用过程:
 
  1. 在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果。

    创建一个html文件:
     
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script>
        function foo(){
          console.log('foo被执行');
        }
        foo();
      </script>
    </body>
    </html>

    控制台:

  2. 如果我们将这个函数编写成一个生成器函数:
        //生成器
        function* foo(){
    
        }
        //iterator 迭代器
        const result = foo();
        console.log(result);

  3. 调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果。
     
        //2、生成器函数的定义
        //生成器函数
        function* foo(){
          yield "aaaa"
          yield "bbbb"
          yield "cccc"
        }
        //iterator 迭代器
        const result = foo();
        console.log(result);
    
        //3、使用迭代器
        //调用一次next,就会消耗一次迭代器
        const res1 = result.next();
        console.log(res1);

    我们使用用完迭代器后看结果:
        //3、使用迭代器
        //调用一次next,就会消耗一次迭代器
        const res1 = result.next();
        console.log(res1);
        const res2 = result.next();
        console.log(res2);
        const res3 = result.next();
        console.log(res3);
        const res4 = result.next();
        console.log(res4);

  4. 研究一下foo生成器函数代码的执行顺序:
     
        //执行顺序
        //2、生成器函数的定义
        //生成器函数
        function* foo() {
          console.log('111')
          yield "aaaa"
          console.log('222')
          yield "bbbb"
          console.log('333')
          yield "cccc"
          console.log('444')
        }
        //iterator 迭代器
        const result = foo();
        console.log(result);
    
        //3、使用迭代器
        //调用一次next,就会消耗一次迭代器
        const res1 = result.next();
        console.log(res1);
        const res2 = result.next();
        console.log(res2);
        const res3 = result.next();
        console.log(res3);
        const res4 = result.next();
        console.log(res4);


    执行foo()后并不会立即打印111,而是在执行第一个next()后foo函数里的第一个log 打印 111,返回结果aaa的iterator对象


    案例:一次生成1-10个数字,需要用的时候再一次去那对应的值
     
        //小练习:一次生成1-10个数字,需要用的时候再一次去那对应的值
        function* generatorNumber(){
          for (let i = 0; i < 10; i++) {
            yield i;
          }
        }
    
        const num = generatorNumber();
        console.log(num.next().value);
  5. generator和promise一起使用
     
        // 6 、generator和Promise结合使用
        function* bar() {
          console.log('1111')
          const result = yield new Promise((resolve, reject) => {
            //模拟异步请求
            setTimeout(() => {
              console.log('即使结束,准备返回结果')
              resolve('3s过去了,generator');
            }, 3000);
          });
          console.log('拿到result:', result);
        }
    
        console.log('开始调用bar()')
        const it = bar();
        console.log('bar()调用完成得到迭代器')
        it.next().value.then(res => {
          console.log('执行完第一次next,拿到then的res')
          it.next(res);
          console.log('执行完第二次next')
        });

    结合上面了解的执行顺序,这里就不难理解了;
    这里需要注意的是next(res),给next传入res,会直接将res返回给result,然后执行:console.log('拿到result:', result); 打印结果result才有值

二十七、redux-saga的使用

redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。redux官网是有提到redux-saga(传送门

依然是上面的案例

Redux-saga的使用步骤如下

  1. 安装redux-saga

    yarn add redux-saga

     

  2. 集成redux-saga中间件

    导入创建中间件的函数;

    通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中;

    启动中间件的监听过程,并且传入要监听的saga;
     
    import { createStore, applyMiddleware, compose } from 'redux';
    import reducer from './reducer.js';
    import thunkMiddleware from 'redux-thunk';
    import createSagaMiddleware from 'redux-saga'
    import mySaga from './saga'
    
    //创建composeEnhancers函数
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace:true}) || compose;
    
    //应用中间件
    //1、引入thunkMiddleware中间件
    //2、引入sagaMiddleware中间件
    const sagaMiddleware = createSagaMiddleware();
    const storeEnhancer = applyMiddleware(thunkMiddleware ,sagaMiddleware);
    const store = createStore(reducer, composeEnhancers(storeEnhancer));
    
    sagaMiddleware.run(mySaga);
    export default store;
    
  3. saga.js文件的编写

    takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)

    put:在saga中派发action不再是通过dispatch,而是通过put;

    all:可以在yield的时候put多个action;
     
    import { takeEvery, put, all, takeLatest } from 'redux-saga/effects';
    import axios from 'axios';
    import { FETCH_SERVICE_SAGA_DATA } from '../store/constants';
    import { changeSagaAction } from './actions';
    
    function* fetchSagaDataAction() {
      const res = yield axios({
        url: 'http://localhost:3000/saga',
      });
      console.log(res);
      yield put(changeSagaAction(res));
    
      //也可以执行多个put
      // yield all([
      //   yield put(changeAction1(data)),
      //   yield put(changeAction2(data))
      // ])
    }
    
    function* mySaga() {
      // takeEvery会监听拦截action事件的类型 FETCH_SERVICE_SAGA_DATA,
      //然后执行fetchSagaDataAction生成器函数
      yield takeEvery(FETCH_SERVICE_SAGA_DATA, fetchSagaDataAction);
    
      // takeLatest takeEvery区别:
      // takeLatest: 依次只能监听一个对应的action
      // takeEvery: 每一个都会被执行
    
      //多个执行放在all函数的数组里
      // yield all([
      //   takeLatest(FETCH_SERVICE_SAGA_DATA, fetchSagaDataAction),
      //   // takeLatest(ADD_NUMBER, fetchHomeMultidata),
      // ]);
    }
    
    export default mySaga;
    
  4. 这里需要在actions.js文件中定义changeSagaAction函数:
    //saga文件中put的函数,用于派发到reducer
     export const changeSagaAction = (sagaData)=> ({
       type: FETCH_SERVICE_SAGA_TYPE,
       sagaData
     })
    这里的FETCH_SERVICE_SAGA_TYPE类型在constants.js添加一个就ok了。
  5. 然后派发到reducer后,需要再更新state,所在reducer.js文件添加:
    case FETCH_SERVICE_SAGA_TYPE:
          return { ...state, sagaData: action.data };
  6. 如何使用呢,还需要在home.js文件中添加对应的mapDispatchToProps映射fetchSagaData,然后在调用就可以触发了:
    fetchSagaData: ()=> {
        dispatch(fetchSagaDataAction)
      }
  7. 这里的fetchSagaDataAction需要传入saga拦截所以需要的type类型对象,我们定义在actions.js文件里:
    // redux-saga拦截的action
    export const fetchSagaDataAction = {
      type: FETCH_SERVICE_SAGA_DATA
    }
  8. 完整代码

    actions.js完整代码:
     
    import axios from 'axios';
    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT,
      FETCH_SERVICE_DATA,
      FETCH_SERVICE_SAGA_DATA,
      FETCH_SERVICE_SAGA_TYPE
    } from './constants.js'
    
    
    export const addAction = num => ({
      type: ADD_NUMBER,
      num,
    });
    
    export const subAction = num =>({
      type:SUB_NUMBER,
      num
    })
    
    export const decrementAction = () =>({ type:DECREMENT })
    
    export const incrementAction = () =>({ type:INCREMENT })
    
    // redux-thunk异步请求
    export const fetchDataAction = (dispatch, getState) => {
      axios({
        url: "http://localhost:3000/data",
      }).then(res => {
        const data = res.data;
        console.log(data);
        dispatch({
          type: FETCH_SERVICE_DATA,
          data
        });
      })
    };
    
    // redux-saga拦截的action
    export const fetchSagaDataAction = {
      type: FETCH_SERVICE_SAGA_DATA
    }
    
    //saga文件中put的函数,用于派发到reducer
     export const changeSagaAction = (sagaData)=> ({
       type: FETCH_SERVICE_SAGA_TYPE,
       sagaData
     })
    
    
    home.js完整代码:
     
    import React, { PureComponent } from 'react'
    import {incrementAction ,addAction ,fetchDataAction ,fetchSagaDataAction} from '../store/actions';
    import {connect} from 'react-redux';
    
    class Home extends PureComponent {
     
      componentDidMount() {
        this.props.fetchData();
        this.props.fetchSagaData();
      }
      render() {
        const {data = []} = this.props;
        return (
          <div>
            <h1>home</h1>
            <h2>当前计数:{this.props.counter}</h2>
            <button onClick = {()=>this.props.increment()}>+1</button>
            <button onClick = {()=>this.props.addNumber(5)}>+5</button>
            <br/>
            <ul>
              {
                data.map((item)=>{
                  return (
                  <li key={item.id}>{item.title}</li>
                  )
                })
              }
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = state => ({
      counter:state.counter,
      data:state.data
    })
    const mapDispatchToProps = dispatch => ({
      increment: ()=> {
        dispatch(incrementAction())
      },
      addNumber: (num)=> {
        dispatch(addAction(num))
      },
      fetchData: ()=> {
        dispatch(fetchDataAction)
      },
      fetchSagaData: ()=> {
        dispatch(fetchSagaDataAction)
      }
    })
    
    export default connect(mapStateToProps ,mapDispatchToProps)(Home)
    
    
    reducer.js完整代码:
     
    import {
      ADD_NUMBER,
      SUB_NUMBER,
      DECREMENT,
      INCREMENT,
      FETCH_SERVICE_DATA,
      FETCH_SERVICE_SAGA_TYPE,
    } from './constants.js';
    const defaultState = {
      counter: 0,
    };
    
    function reducer(state = defaultState, action) {
      switch (action.type) {
        case INCREMENT:
          return { ...state, counter: state.counter + 1 };
        case DECREMENT:
          return { ...state, counter: state.counter - 1 };
        case ADD_NUMBER:
          return { ...state, counter: state.counter + action.num };
        case SUB_NUMBER:
          return { ...state, counter: state.counter - action.num };
        case FETCH_SERVICE_DATA:
          return { ...state, data: action.data };
        case FETCH_SERVICE_SAGA_TYPE:
          return { ...state, sagaData: action.data };
        default:
          return state;
      }
    }
    
    export default reducer;
    
  9. 运行结果:

二十八、中间件

1、打印日志需求

注意:案例使用第七点搭建的learn-redux项目的基础上进行修改

  1. 前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:

    比如我们现在有一个需求,在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store  、state;

    也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作;

  2. 如果没有中间 件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。

    在index.js文件中:

     
    //注意,node环境路径必须写全,只有在webpack环境下才能省略:/index.js
    import store from './store/index.js';
    import { addAction, subAction } from './store/actions.js';
    
    //1、基本方法----------------------
    console.log('dispatch前---dispatch action' ,addAction(5));
    store.dispatch(addAction(5));
    console.log('dispatch前---dispatch action' ,store.getState());
    
    console.log('dispatch前---dispatch action' ,addAction(10));
    store.dispatch(subAction(10));
    console.log('dispatch前---dispatch action' ,store.getState());

     

  3. 但是这种方式缺陷非常明显:

    首先,每一次的dispatch操作,我们都需要在前面加上这样的逻辑代码;

    其次,存在大量重复的代码,会非常麻烦和臃肿;
  4. 是否有一种更优雅的方式来处理这样的相同逻辑呢?

    我们可以将代码封装到一个独立的函数中
     
    //2、封装一个函数--------------------
    function dispatchAndLoggin(action) {
      console.log('dispatch前---dispatch action', action);
      store.dispatch(addAction(5));
      console.log('dispatch前---dispatch action', store.getState());
    }
    dispatchAndLoggin(addAction(5))
    dispatchAndLoggin(addAction(10))

     
  5. 但是这样的代码有一个非常大的缺陷:

    调用者(使用者)在使用我的dispatch时,必须使用我另外封装的一个函数dispatchAndLog;

    显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用dispatch;
  6. 修改dispatch

     我们对代码进行如下的修改:利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
    //3、在函数的基础上进行优化:修改原有的dispatch--------------------
    //hack技术: monkeyingpatch   修改原有api的能力
    let next = store.dispatch;
    function dispatchAndLoggin(action) {
      console.log('dispatch前---dispatch action', action);
      next(addAction(5));
      console.log('dispatch前---dispatch action', store.getState());
    }
    store.dispatch = dispatchAndLoggin;
    
    store.dispatch(addAction(5))
    store.dispatch(addAction(10))
    这样就意味着我们已经直接修改了dispatch的调用过程;

    在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog;
     
  7. 当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store进行这样的处理
     
    //4、将上面的代码封装成一个函数--------------------
    //当需要使用我们的这块代码的逻辑的时候,只需要调用该函数,这个打印日志功能就会生效
    //可以把这部分代码封装到一个独立的文件中并export出这个函数即可
    function patchLogging(store) {
      let next = store.dispatch;
      function dispatchAndLoggin(action) {
        console.log('dispatch前---dispatch action', action);
        next(addAction(5));
        console.log('dispatch前---dispatch action', store.getState());
      }
      store.dispatch = dispatchAndLoggin;
    }
    
    patchLogging(store)
    store.dispatch(addAction(5))
    store.dispatch(addAction(10))
  8. 实现thunk需求:

    • redux-thunk的作用:

      我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数;

      那么redux-thunk中的基本实现过程是怎么样的呢?事实上非常的简单。
    • 我们来看下面的代码:

      我们又对dispatch进行转换,这个dispatch会判断传入的是对象还是函数
       
      //5、将上面的代码封装成一个函数--------------------
      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;
      }
      
      patchThunk(store);
      
      //传入对象
      store.dispatch(addAction(5));
      store.dispatch(addAction(10));
      
      //传入函数
      function foo(dispatch ,getState){
        console.log(dispatch ,getState)
        dispatch(subAction(10))
      }
      store.dispatch(foo);

2、合并中间件

  1. 单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
     
    function applyMiddlewares(...middlewares){
      middlewares.forEach(middleware => {
        store.dispatch = middleware(store);
      });
    }
    完整代码:
    //6、封装 applyMiddleware --------------------
    
    function patchThunk(store) {
      let next = store.dispatch;
    
      function dispatchAndThunk(action) {
        if (typeof action === 'function') {
          action(store.dispatch, store.getState);
        } else {
          next(action);
        }
      }
      //直接返回这个dispatchAndThunk,统一在middleware中调用
      return dispatchAndThunk;
    }
    
    function patchLogging(store) {
      let next = store.dispatch;
      function dispatchAndLoggin(action) {
        console.log('dispatch前---dispatch action', action);
        next(addAction(5));
        console.log('dispatch前---dispatch action', store.getState());
      }
      //直接返回这个dispatchAndLoggin,统一在middleware中调用
      return dispatchAndLoggin;
    }
    
    function applyMiddlewares(...middlewares){
      middlewares.forEach(middleware => {
        //为了保证middleware是个纯函数,所有在此处进行dispatch的一个负值操作
        store.dispatch = middleware(store);
      });
      //如果要保证applyMiddlewares是一个纯函数,也可以在此处return store
    }
    
    applyMiddlewares(patchThunk ,patchLogging)

     

  2. 我们来理解一下上面操作之后,代码的流程:

  3. 真实的中间件实现起来会更加的灵活,有兴趣可以参考redux合并中间件的源码流程。

二十九、Reducer

1、Reducer代码拆分

  1. 为什么这个函数叫reducer?官方就就叫reduder(传送门

    因为我们这个纯函数的作用和Array.prototype.reduce(reducer, ?initialValue)的作用非常相似



  2. 我们来看一下目前我们的reducer: (继续上面的代码)


    当前这个reducer既有处理counter的代码,又有处理home页面的数据;

    后续counter相关的状态或home相关的状态会进一步变得更加复杂;

    我们也会继续添加其他的相关状态;

    如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
  3. 因此,我们可以对reducer进行拆分:

    我们先抽取一个对counter处理的reducer;

    再抽取一个对home处理的reducer;

    将它们合并起来;

2、Reducer文件拆分

 目前我们已经将不同的状态处理拆分到不同的reducer中,但是依然有弊端:

  • 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
  • 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;

3、combineReducers函数

  1. 目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。

  2. 事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:

  3. 那么combineReducers是如何实现的呢?

    事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);

    在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;

    新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;

    在上面拆分好文件的项目中的reducer.js文件中将:

     
    function reducer(state = {}, action) {
      return {
        aboutInfo: aboutReducer(state.aboutInfo, action),
        homeInfo: homeReducer(state.homeInfo, action)
      }
    }
    替换为:
    const reducer = combineReducers({
      aboutInfo: aboutReducer,
      homeInfo: homeReducer
    });

    运行依然ok

4、combineReducers源码解读


 
 
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值