JS状态容器—Redux与React-Redux及中间件使用

基础

什么是Redux?

Redux是JavaScript状态容器,提供可预测化的状态管理。可以让你构建一致化的应用,运行于不同的环境。
Redux工作流向图
Redux工作流向图

安装Redux

npm install --save redux
#或者
yarn add redux

核心思想

Redux核心思想是通过action来更新state。

Action就像是描述发生了什么的指示器。最终为了把action和state串起来,开发一些函数,这些函数叫做reducer。reducer只是一个接收state和action并返回新的state的函数。

对于大应用来说,不大可能仅仅只写一个这样的函数,所以我们编写很多小函数来分别管理state的一部分,最后通过一个大的函数调用这些小函数,进而管理整个应用的state。

三大原则

单一数据源

整个应用的state被存储在一棵object tree中,并且这个object tree只存在于唯一一个store中。

console.log(store.getState())
/* 输出
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/
State只读

唯一改变state的方法就是触发action,action是一个用于描述已发生事件的普通对象。

这样确保了视图和网络请求都不能直接修改state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个顺序执行,因此不用担心竞态条件的出现。

Action就是普通对象而已,因此它们可以被日志打印、序列化、存储、后期调试或测试回放出来。

//定义Action对象,并通过store.dispatch方法触发Action
store.dispatch({
  type:'COMPLETE_TODO',
  index:1
})

store.dispatch({
  type:'SET_VISIBILITY_FILTER',
  filter:'SHOW_COMPLETED'
})
使用纯函数来执行修改

为了描述action如何改变 state tree,你需要编写reducers函数

Reducer只是一些纯函数,它接收先前的state和action,并返回新的state。

刚开始你可能只有一个reducer,随着应用的变大,你可以把它拆成多个小的reducers,分别独立的操作state tree的不同部分,因为reducers只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的reducer函数来处理一些通用任务。

//1. 导入redux中的combineReducers、createStore对象
import {combineReducers,createStore} from 'redux';


//2. 定义多个小的reducer函数处理特定的state(参数为先前的state和action)
function visibilityFilte(state = 'SHOW_ALL' , action){
  switch(action.type){
    case 'SET_VISIBBILITY_FILTER':
      return action.filter
    default:
      return state
  }
 }

function todos(state = [] , action){
  switch(action.type){
    case 'ADD_TODO':
      return [
        ...state,
        {
          text:action.text,
          completed:false,
        }
      ]
    case 'COMPLETED_TODO':
      return state.map((todo,index)=>{
        if(index === action.index){
          return Object.assign({},todo,{
            completed:true
          })
          
        }
        return todo
      })
    default:
      return state     
  }
}

//3. 通过combineReducers函数将多个小的reducers函数组合。
let reducer = combineReducers({visibilityFilte,todos})

//4. 通过reducer函数创建Redux Store对象来存放应用状态
let store = createStore(reducer);

//5. 可以手动订阅更新,也可以事件绑定到视图层
store.subscribe(()=>{
  //当state更新会触发这里
  console.log(store.getState());
});

//6. 通过store指定action来触发Action改变state,
store.dispatch({
  type:'COMPLETE_TODO',
  index:1
})

store.dispatch({
  type:'SET_VISIBILITY_FILTER',
  filter:'SHOW_COMPLETED'
})

Action

  • 简介

    Action 是把数据从应用(这里之所以不叫View是因为这些数据有可能是从服务器响应,用户输入或其他非View的数据)传到store的有效载荷。它是store数据的唯一来源。一般来说你会通过store.dispatch()将action传到store。(简单说:action用于描述发生了什么)

    添加新的todo任务的action是这样的:

    const ADD_TODO = 'ADD_TODO'
    
    {
      type:ADD_TODO,
      text:'Build my first Redux app'
    }
    

    Action本质是JavaScript的普通对象。我们约定,action内必须使用一个字符串类型的type字段来表示将要执行的动作。多数情况下,type会被定义成字符串常量。当应用规模越来越大时,建议使用独立的模块或文件存放action。

    import {ADD_TODO,REMOVE_TODO} from '../actionTypes'
    

    除了type字段外,action对象的结构完全由你自己决定,参照 Flux 标准 Action 获取关于如何构造 action 的建议。

    这时,我们还需要再添加一个action index来表示用户完成任务的动作序列号。因为数据存放在数组中的,所以我们通过下标Index来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的ID作为数据的引用标识。

    {
      type:TODO_ADD,
      index:5
    }
    

    我们应该尽量减少在action中传递数据。比如上面的例子,传递index就比把整个任务对象传过去要好

  • Action创建函数(Action Creator)

    Action创建函数就是生成action的方法。actionaction创建函数这两个概念很容易混在一起,使用时最好注意区分。

    在Redux中的action创建函数只是简单的返回一个action:

    function addTOdo(text){
      return {
        type:ADD_TODO,
        text
      }
    }
    

    这样做将使得action创建的函数更容易被移植和测试。

    store里能直接通过store.dispatch()调用dispatch()方法,但是多数情况下你会使用react-redux提供的connect()帮助器来调用。bindActionCreators()可以自动把多个action创建的函数绑定到dispatch()方法上。

    注意:我们通常使用此中方式(action的工厂函数/action creator)构造action对象)

  • 源码案例actions.js

    //action 类型(大型项目一般会独立在一个组件中声明,然用导出供其他组件使用)
    export const ADD_TODO = 'ADD_TODO';
    export const TOGGLE_TODO = 'TOGGLE_TODO';
    export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
    
    //其他常量对象
    export const VisibilityFilters = {
      SHOW_ALL:'SHOW_ALL',
      SHOW_COMPLETED:'SHOW_COMPLETED',
      SHOW_ACTIVE:'SHOW_ACTIVE'
    }
    
    //创建action函数并导出,返回action
    export function addTodo(text){
      return {
        type:ADD_TODO,
        text
      }
    }
    
    export function toggleTodo(index){
      return {
        type:TOGGLE_TODO,
        index
      }
    }
    
    export function setVisibilityFilter(filter){
      return {
        type:SET_VISIBILITY_FILTER,
        filter
      }
    }
    

Reducer

  • 简介

    Reducers指定了应用状态如何响应actions并发送到store的,记住actions只是描述了有事情发生这一事实,并没有描述应用如何更新state。(简单说:reducer根据action更新state)

    整个应用只有一个单一的 reducer 函数:这个函数是传给 createStore 的第一个参数。一个单一的 reducer 最终需要做以下几件事:

    • reducer 第一次被调用的时候,state 的值是 undefined。reducer 需要在 action 传入之前提供一个默认的 state 来处理这种情况。
    • reducer 需要先前的 state 和 dispatch 的 action 来决定需要做什么事。
    • 假设需要更改数据,应该用更新后的数据创建新的对象或数组并返回它们。
    • 如果没有什么更改,应该返回当前存在的 state 本身。

    注意:保持reducer纯净非常重要,永远不要在reducer里做这些操作

    • 修改传入参数
    • 执行有副作用的操作,例如:请求和路由跳转;
    • 调用非纯净函数,如:Date.now()Math.random()

    只需要谨记reducer一定要保持纯净。只要传入参数相同,返回计算得到的下一个state就一定相同。没有特殊情况、没有副作用、没有API请求、没有变量修改,单纯执行计算。

  • Action处理

    Redux首次执行时,state为undefined,此时我们可借机设置并返回应用的初始state。

    //引入 VisibilityFilters 常量对象
    import {VisibilityFilters} from './actions'
    
    //初始化state状态。
    const initialState={
      visibilityFilter:VisibilityFilters.SHOW_ALL,
      todos:[]
    };
    
    //定义reducer函数
    function todoApp(state,action){
      //如果state为定义返回初始化的state
      if(typeof state === 'undefined'){
           return initialState;
         }
       //这里暂不处理任何action,仅返回传入的state
      return state
    }
    

    使用ES6参数默认值语法精简代码

    function todoApp(state = initialState,action){
        //这里暂不处理任何action,仅返回传入的state
        return state;
    }
    

    现在可以处理action.type SET_VISIBILITY_FILTER。需做的只是改变state中的visibilityFilter:

    function todoApp(state = initialState,action){
      switch(action.type){
        case 'SET_VISIBILITY_FILTER':
          return Object.assign({},state,{
            visibilityFilter:action.filter
          })
        default:
          return state;
      }
    }
    

    注意:

    1. 不要修改state

      使用Object.assign() 新建了一个副本。不要使用下面方式

      Object.assign(state,{visibilityFilter:action.filter}),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。

    2. default情况下返回旧的state。遇到未知的action时,一定要返回旧的state

      Object.assign介绍

      Object.assign(target,source1,source2...sourceN)是ES6特性,用于对象的合并。

      //对象合并:source1,source2合并到target中。
      const target = { a: 1 };
      const source1 = { b: 2 };
      const source2 = { c: 3 };
      Object.assign(target, source1, source2);
      target // {a:1, b:2, c:3}
      
      //同名属性的替换:source中a属性值替换掉target中的a属性值。
      const target = { a: { b: 'c', d: 'e' } }
      const source = { a: { b: 'hello' } }
      Object.assign(target, source)// { a: { b: 'hello' } }
      
      //数组的处理:assign会把数组视为属性名为 0,1,2 的对象,
      //因此源数组的0号属性值4覆盖了目标数组的0号属性值1。
      Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]
      
      

      注意:

      • 该方法的第一个参数是目标对象,后面的参数都是源对象;
      • 如果目标对象与源对象或多个源对象有同名属性,则后面的属性会覆盖前面的属性;
      • Object.assign是浅拷贝。如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用,这个对象的任何变化,都会反映到目标对象上面;
      • 同名属性的替换。对于嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
  • 处理多个Action

    注意

    • 每个reducer只负责管理全局state中它负责的一部分。每个reducer的state参数都不同,分别对应它管理的那部分state数据。

    最后。Redux提供了combineReducers()工作类,用于生成一个函数,这个函数来调用你的一系列的reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成的函数再将所有的reducer的结果合并成一个大的对象。

  • 代码案例reducers.js

    import {combineReducers} from 'redux'
    //从actions文件中中导入action type常量和函数
    import {
      ADD_TODO,
      TOGGLE_TODO,
      SET_VISIBILITY_FILTER,
      VisibilityFilters
    } from './actions'
    
    //解构赋值
    const {SHOW_ALL} = VisibilityFilters
    
    //定义显示筛选的reducer函数
    function visibilityFilter(state = SHOW_ALL,action){
      switch(action.type){
        case SET_VISIBILITY_FILTER:
          return action.filter
        default:
          return state   
      }
    }
    
    //定义处理事物的reducer函数
    function todos(state = [] , action){
      switch(action.type){
        case ADD_TODO:
          return [
            ...state,
            {
              text:action.text,
              completed:false 
            } 
          ]
        case TOGGLE_TODO:
          return state.map((todo,index)=>{
            if(index ==== action.index){
              return Object.assign({},todo,{
                completed:!todo.completed
              })
             }
            return todo
          })
        default:
          return state   
      }
    }
    
    //通过combineReducer函数将自定义的多个reducers关联起来
    const todoApp = combineReducers(){
      visibilityFilter,
      todos
    }
    
    //导出该Reducer供外界使用
    export default todoApp;
    

Store

  • 简介

    前面介绍了action来描述“发生了什么”,使用reducers来根据action更新state的用法。

    Store 就是把action和reducer联系到一起的对象。Store有以下职责:

    • 维持应用的state;
    • 提供getState()方法获取state;
    • 提供dispatch(action)方法更新state;
    • 通过subscribe(listener)注册监听器;
    • 通过subscribe(listener)返回的函数unsubscribe用于注销监听器。

    再次强调一下Redux应该只有一个单一的store。当要拆分数据处理逻辑时,你应该使用reducer组合而不是创建多个store。

  • createStore创建Store

    根据已有的reducer来创建store是非常容易的,前一节中我们通过combineReducers()将多个reducer合并为一个。现我们将其导入,通过其使用createStore(reducers)创建Store:

    import {createStore} from 'redux'
    //导入定义好的reducers对象
    import todoApp from './reducers'
    //创建Store对象
    let store = createStore(todoApp)
    

    createStore()的第二个参数是可选的,用于设置state的初始状态。这对开发同构应用时非常有用,服务器端redux应用的state结构可以于客户端保持一致,那么客户端可以将从网络接收到的服务端state直接用于本地数据初始化:

    let store createStore(todoApp,window.STATE_FROM_SERVER)
    
  • 发起Actionindex.js

    经过上述几个步骤,我们已经创建了actionreducerstore了,此时我们可以验证一下,虽然没有页面,因为它们都是纯函数,只需要调用一下,对返回值做判断即可。写测试就这么简单。

    import {createStore} from 'redux'
    
    //1. 引入定义好的action
    import {
      addTodo,
      toggleTodo,
      setVisibilityFilter,
      VisibilityFilters} from './actions'
    
    //2. 引入定义好的reducer
    import todoApp from './reduces'
    
    //3. 创建store
    let store = createStore(todoApp)
    
    //4. 触发action通过reducer更新state
    store.dispatch(addTodo('Learn about actions'))
    store.dispatch(addTodo('Learn about reducers'))
    store.dispatch(addTodo('Learn about store'))
    store.dispatch(toggleTodo(0))
    store.dispatch(toggleTodo(1))
    store.dispatch(setVisibilityFilter(VisibilityFilter.SHOW_COMPLETED))
    
    //5. 通过subscribe开启监听state更新,注意返回一个函数对象用于注销监听
    const unsubscribe = store.subscribe(()=>{
      console.log(store.getState())
    })
    
    //6. 停止监听state更新
    unsubscribe();
    
    

State的基本结构

Redux 鼓励你根据需要管理的数据来思考你的应用程序。数据就是你的应用state。

Redux state中顶层的状态树通常是一个普通的JavaScript对象(当然也可以是其他类型的数据,比如:数字、数据或者其他专门的数据结构,但大多数库的顶层值都是一个普通对象)。

大多数应用会处理多种数据类型,通常可以分为以下三类:

  • 域数据(Domain data):应用需要展示、使用或者修改的数据;
  • 应用状态(App state):特定与应用某个行为的数据;
  • UI状态(UI state):控制UI如何展示的数据。

一个典型的应用state大致会长这样:

{
  domainData1:{},   //数据域1
  domainData2:{},   //数据域2
  appState1:{},     //应用状态域1
  appState2:{},     //应用状态域1
  ui:{              //UI域
    uiState1:{},
    uiState2:{},
  } 
}

React-Redux 使用

这里强调一下Redux和React之间没有任何关系。Redux支持React、Angular、Ember、JQuery、甚至纯JavaScript。

尽管此次,Redux还是和React和Deku这类库搭配使用最好,因为这类库允许你以state函数的形式来描述界面,Redux通过action的形式来发起state变化。

安装React Redux

Redux默认并不包含React绑定库,需要单独安装。

npm install --save react-redux
#或者
yarn add react-redux
核心API讲解
1. Provider

provider组件是react-redux提供的核心组件,作用是将Redux Store提供可供内部组件访问。

使用

通过react-redux提供的Provider组件包括其他组件

//导入Provider组件
import {Provider} from 'react-redux';
//导入创建好的Redux Store对象
import store from './store';
//导入组件
import CustomComponent from './CustomComponent';

export default class App extends Component{
  render(){
    return(
      <Provider>
      		<CustomComponent/>
      </Provider>
    );
  } 
}
2. connect

connect组件也是react-redux提供的核心组件,作用是将当前组件与Redux Store进行关联,以便通过mapStateToProps、mapDispatchToProp函数将当前组件Props与Redux Store 中的State和dispatch建立映射。

注意:

  1. 使用 connect() 前,需要先定义 mapStateToProps;
  2. 使用connect连接的组件需要被Provider组件包裹;
  3. mapStateToProps:这个函数来指定将当前组件Props与 Redux store state 建立映射关系。在每次 store 的 state 发生变化的时候,应用内所有组件的该函数都会被调用。如果不传组件不会监听Store State的变化,也就是说Store的更新不会引起UI的更新。
  4. mapDispatchToProps:这个函数用来指定将当前组件Props与store.dispatch建立映射关系。如果不传React-Redux会自动将dispatch注入组件的props(可通过this.props.dispatch(action)使用)。

使用

在Provider组件包裹的组件通过react-redux提供的connect将组件与Redux Store进行连接。

//1. 引入 connect
import { connect } from 'react-redux';
class CustomComponent extends Component{
  render(){
    return (
       <View style={{flex: 1, flexDirection: 'column', alignItems: 'center'}}>
      		<Button title={'修改显示数据'} 
                /**
                 *通过this.props.btnOnClick指定函数来间接触发store.dispatch(action)
                 *更改Store中State。
                 */
                onPress={() => {this.props.btnOnClick();}}/>
          //通过属性映射this.props.showText获取到Redux Store State中的数据。
        	<Text style={{marginTop: 20}}>{this.props.showText}</Text>
      </View>
    );
  }
}

/**
 *2.创建mapStateToProp函数
 */
const mapStateToProps = (state) => {
    return {
        /**
         *将Redux Store State中的loginText映射到当前组件的showText属性上,
         *然后当前组件通过this.props.showText即可获取存储在Store State中的loginText对应的值。
         */
        showText: state.loginText, 
        //将Redux Store State中的loginStatus状态映射到当前组件的showStatus属性上。
        showStatus: state.loginStatus,
    }
};

/**
 *3.创建mapDispatchToProp函数
 */
const mapDispatchToProps = (dispatch)=>{
  	return{
      /**
       *changeShowTex:为自定义的Props属性函数名,当前组件通过this.props.changeShowText()
       *即可触发store.dispatch(action)来更新Redux Store State中数据。
       */
      changeShowText:()=>{
        dispatch(action);   //发送指定的action来更改Store中的State
      }
  	}
}

/**
 *4.通过connect函数将当前组件与Redux的Store连接起来。(当前组件需要被Provider组件包裹的)
 */
export default connect(mapStateToProps,mapDisPatchToProps)(CustomComponent);
完整示例代码

实现目标:通过点击页面按钮(触发store.dispatch(action)),来更改当前显示的文案信息(这里的文案显示信息存储在Redux Store State中)

  1. 定义Action 类型

    //ActionType.js
    //登陆状态
    const LOGIN_TYPE = {
        LOGIN_SUCCESS: 'LOGIN_SUCCESS',
        LOGIN_FAILED: 'LOGIN_FAILED',
        LOGIN_WAITING: 'LOGIN_WAITING',
    };
    export {LOGIN_TYPE};
    
  2. 创建Action(告诉Reducer要做什么操作)

    //Actions.js
    //导入action types
    import {LOGIN_TYPE} from '../ActionType';
    
    export const loginWaiting = {
        type: LOGIN_TYPE.LOGIN_WAITING,
        text: '登陆中...',
    };
    
    export const loginSuccess = {
        type: LOGIN_TYPE.LOGIN_SUCCESS,
        text: '登陆成功...',
    };
    
    export const loginFailed = {
        type: LOGIN_TYPE.LOGIN_FAILED,
        text: '登陆失败...',
    };
    
  3. 创建Reducer(根据传递进来的action type来处理相应逻辑返回新的state)

    //Reducer.js
    //导入action type
    import {LOGIN_TYPE} from '../ActionType';
    
    //默认的state
    const defaultState = {
        loginText: '内容显示区',
        loginStatus: 0,
    };
    
    /**
     *创建reducer:
     *根据当前action类型更改Store State中的loginText和loginStatus,
     *会回调发送store.dispatch(action)事件组件的mapStateToProps函数。
     */
    const AppReducer = (state = defaultState, action) => {
        switch (action.type) {
            case LOGIN_TYPE.LOGIN_WAITING:    
                return {
                    loginText: action.text,
                    loginStatus: 0,
                };
            case LOGIN_TYPE.LOGIN_SUCCESS:
                return {
                    loginText: action.text,
                    loginStatus: 1,
                };
            case LOGIN_TYPE.LOGIN_FAILED:
                return {
                    loginText: action.text,
                    loginStatus: 2,
                };
            default:
                return state;
        }
    };
    export {AppReducer};
    
  4. 创建Redux Store

    //store.js
    import {createStore} from "redux";
    import {AppReducer} from '../reducers/AppReducer';
    //依据Reducer创建store
    const store = createStore(AppReducer);
    export default store;
    
  5. 使用入口

    //App.js
    //通过react-redux提供的Provider组件将store传递给子组件访问
    import React,{Component} from 'react';
    import {Provider} from 'react-redux';
    import store from './store';
    import CustomComponent from '../page/CustomComponent';
    import CustomComponent2 from '../page/CustomComponent2';
    import CustomComponent3 from '../page/CustomComponent3';
    export default class App extends Component{
      render(){
        return(
          <Provider store={store}>
          	  <CustomComponent/>    //子组件
              <CustomComponent2/>   //子组件2
              <CustomComponent3/>   //子组件3
              ...
          </Provider>
        )
      } 
    }
    

    上面我们通过react-redux提供的Provider组件将我们创建好的store提供给子组件CustomComponent访问,接下来我们看看子组件中如何与Redux Store建立关系,并访问其State中内容。

  6. 子组件中访问Redux Store State数据(以CustomComponent为案例,其他子组件一样)

    //CustomComponent.js
    import React,{Component} from 'react';
    import {View,Text,Button} from 'react-native';
    
    //导入Action,下面业务点击要触发dispatch(action)
    import * as Actions from '../actions/Actions';
    
    //1.导入connect,下面需要将当前组件与Redux Store通过该connect建立连接。
    import {connect} from 'react-redux';
    
    export default class CustomComponent extends Component{
      render(){
        return(
           <View style={{flex: 1, flexDirection: 'column', alignItems: 'center'}}>
                 /**
                  *5.当前组件通过this.props.xxx 指定mapStateToProps函数中自定义的属性来获取
                  *	 从Redux Store State映射的值。
                  */
                 <Text style={{marginTop: 20}}>{this.props.showText}</Text>
    
                 <Button title={'模拟登陆中'} onPress={() => {
                     /**
                      *6.当前组件通过this.props.xxx() 调用mapDispatchToProps自定义的函数,
                      *以此间接触发store.dispatch(action)来发送action达到更新Store中State目的
                      */
                     //当connect第二个参数不传递的时候,Redux Store会自动将dispatch映射到Props上。
                     //this.props.dispatch(Actions.loginWaiting);} 
                     this.props.btnOnClick(1);}
                 }/>
                 <Button title={'模拟登陆成功'} onPress={() => {
                     this.props.btnOnClick(1);}
                 }/>
                 <Button title={'模拟登陆失败'} onPress={() => {
                     this.props.btnOnClick(2);}
                 }/>
             </View>
        )
      }
    }
    
    /**
     *2.定义mapStateToProps函数(当Store中的State变化时候,会回调改函数)
     *	返回一个Object,内部是将state中的值映射到自定义的属性上,以便当前组件通过this.props.xxx来
     *	获取State中数据。
     */
    const mapStateToProps = (state)=>{
      	return{
            //将Redux Store State中的loginText映射到自定义的showText属性上。
          	showText:state.loginText,
            //将Redux Store State中的loginStatus映射到自定义的showStatus属性上。
          	showStatus:state.loginStatus,
        }
    }
        
    /**
     *3.定义mapDispatchToProps函数:
     *	返回一个Object,内部定义的属性函数名称,以便当前组件通过调用this.props.xxx()
     *	来间接触发store.dispatch(action)。
     */
    const mapDispatchToProps = (dispatch)=>{
      return {
        changeShowText:(type)=>{
          switch(type){
            case 0:
              dispatch(Actions.loginWaiting);
              break;
            case 1:
              dispatch(Actions.loginSuccess);
              break;
            case 2:
               dispatch(Actions.loginFailed);
              break;   
           }
        }
      }
    }
    
    /**
     *4.通过connect将当前CustomComponent组件与Redux Store建立连接,并通过mapStateToProps、
     *	mapDispatchToProps函数将Redux Store State映射到当前组件Props中。
     */
    export default connect(mapStateToProps,mapDispatchToProps)(CustomComponent);
    
扩展:
1. 嵌套组件中访问Redux Store State

如下组件:

根组件APP.js

return(
  <Provider>
    <CustomComponent/>
  </Provider>
)

子组件CustomComponent.js

//内部引入Child组件
return(
  ...
  <Child/>
)

我们在Child组件中如果要访问Redux Store State与CustomComponent组件访问方式一样,如下:

Child.js

import React,{Component} from 'react';
import {View, Text, Button} from 'react-native';
//1.导入connect
import {connect} from 'react-redux';
import * as Actions from '../actions/CommonAction';
export default class Child extends Component{
  render(){
    return(
      <View>
        /**
         *使用:通过this.props.xxxx 指定mapStateToProps定义的属性名
         *获取Store State映射的数据。
         */
      	<Text>{this.props.xxxx}</Text>
        <Button onPress={()=>{
           /**
            *通过this.props.xxxx()调用mapStateToProps声明的函数
            *间接触发store.dispatch(action)来更新Redux Store State。
            */
           this.props.xxx();
        }}>
      </View>
    );
  }
  
//2.定义mapStateToProps函数
const mapStateToProps = (state)=>{
    return{
      //TODO...
    }
}

//3.定义mapDispatchToProps函数
const mapStateToProps = (dispatch)=>{
    return{
      //TODO...
    }
}

/**
 *4.通过connect将当前组件与Redux Store建立连接,并通过mapStateToProps、mapStateToProps函数
 *将Store 的 State和dispatch映射到Props中
 */
export default connect(mapStateToProps,mapStateToProps)(Child);

}
2. 使用combineReducers合并多个零散Reducer

上面的代码中我们的Action以及Reducer都定义在一个文件中,对于中大型项目后期错误的排查和维护比较困难,因此我们重构项目,将Action和Reducer依据业务功能拆分使其各自独立,通过借助combineReducers对多个Reduce进行合并。

比如我们有登陆、注册页面,因此我们将原来的Action拆分成LoginAction、RegisterAction;将原来的Reducer拆分成LoginReducer、RegisterReducer使其各司其职处理相关的业务。

  1. 拆分Action

    LoginAction.js

    /**
     *登陆Action
     *PS:目前action触发携带的是静态数据,内部的data都是写好的,
     *后面会扩展通过接口请求返回数据填充到data中
     */
    import {LOGIN_TYPE} from './ActionType';
    export const loginWaiting = {
        type: LOGIN_TYPE.LOGIN_WAITING,
        data: {
            status: 10,
            text: '登陆中...',
        },
    };
    export const loginSuccess = {
        type: LOGIN_TYPE.LOGIN_SUCCESS,
        data: {
            status: 11,
            text: '登陆成功!',
        },
    };
    export const loginFailed = {
        type: LOGIN_TYPE.LOGIN_FAILED,
        data: {
            status: 12,
            text: '登陆失败!',
        },
    };
    

    RegisterAction.js

    /**
     *注册Action
     */
    import {REGISTER_TYPE} from './ActionType';
    export const registerWaiting = {
        type: REGISTER_TYPE.REGISTER_WAITING,
        data: {
            status: 20,
            text: '注册中...',
        },
    };
    export const registerSuccess = {
        type: REGISTER_TYPE.REGISTER_SUCCESS,
        data: {
            status: 21,
            text: '注册成功!',
        },
    };
    export const registerFailed = {
        type: REGISTER_TYPE.REGISTER_FAILED,
        data: {
            status: 22,
            text: '注册失败!',
        },
    };
    
  2. 拆分Reducer

    LoginReducer.js

    //默认登陆页面属性
    const defaultLoginState = {
        Ui: {
            loginStatus: '',   //登陆状态(用于控制登陆按钮是否可点击、以及显示加载框等)
            loginText: '',     //登陆不同状态下的提示的文字
        },
    };
    const LoginReducer = (state = defaultLoginState, action) => {
        switch (action.type) {
            case LOGIN_TYPE.LOGIN_WAITING:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            case LOGIN_TYPE.LOGIN_SUCCESS:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            case LOGIN_TYPE.LOGIN_FAILED:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            default:
                return state;
        }
    };
    export default LoginReducer;
    

    RegisterReducer.js

     //注册页面默认状态
    const defaultRegisterState = {
        Ui: {
            registerStatus: '',   //登陆状态(用于控制登陆按钮是否可点击、以及显示加载框等)
            registerText: '',     //登陆不同状态下的提示的文字
        },
    };
    const RegisterReducer = (state = defaultRegisterState, action) => {
        switch (action.type) {
            case REGISTER_TYPE.REGISTER_WAITING:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            case REGISTER_TYPE.REGISTER_SUCCESS:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            case REGISTER_TYPE.REGISTER_FAILED:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            default:
                return state;
        }
    };
    export default RegisterReducer;
    

    通过combineReducer({key1:reducer1,key2:reducer2})将reducer组合

    注意:

    • 使用combineReducers进行组合Reducer时候我们可指定Reducer名称key,也可省略(默认使用Reducer导出的组件名)。
    • 通过combineReducers组合后,在展示组件中通过mapStateToProps函数映射时候我们需要指定combineReducers合并时指定的Reducer名称来访问Redux Store State中的数据(见下面案例)
    //合并Reducer
    import LoginReducer from './LoginReducer';
    import RegisterReducer from './RegisterReducer';
    const AppReducers = combineReducers({
        LoginReducer,                   //没有指定LoginReducer名称,Redux默认使用LoginReducer
        registerReducer:RegisterReducer,//指定LoginReducer名称为registerReducer,
    });
    export default AppReducers;
    

    后面的Reducer使用不变,通过指定AppReducers使用createStore来创建Store。

    重构以后运行看一下我们的State中的数据格式如下:

    /**
     *通过combineReducers组合后的Reducer,在Redux Store State中会
     *自动为不同的Reducer添加名称区分各自数据状态区
     */
    {
    	"LoginReducer": {
    		"Ui": {
    			"loginStatus": 10,
    			"loginText": "登陆中..."
    		}
    	},
    	"registerReducer": {
    		"Ui": {
    			"registerStatus": "",
    			"registerText": ""
    		}
    	}
    }
    

    接下来我们在展示组件的mapStateToProps中将Redux Store State映射到展示组件的Props中

    登陆页面/组件(Login.js)

    //定义connect函数第一个参数:将Store中的state映射到当前组件的props上
    const mapStateToProps = (state) => {return {
            /**
             *这里我们通过state.xxx方式将问Redux Store State中属性映射到当前组件Props属性上。
             *其中[xxx] 为combineReducers合并Reducer时指定的名称(没指定默认使用组件导出名)
             */
            loginShowText: state.LoginReducer.Ui.loginText,
            loginShowStatus: state.LoginReducer.Ui.loginStatus,
        };
    };
    

    注册页面/组件(Register.js)

    //定义connect函数第一个参数:将Store中的state映射到当前组件的props上
    const mapStateToProps = (state) => {
        return {
            /**
             *通过state.xxxx 指定 combineReducers 合并Reducer时指定的名称,
             *将Redux Store State属性映射到展示组件的属性上。
             */
            registerShowText: state.registerReducer.Ui.registerText,
            registerShowStatus: state.registerReducer.Ui.registerStatus,
        };
    };
    

    重构后的项目结构如下,这样我们就可以根据业务模块进行针对性的处理Action和Reducer内业务逻辑,使其逻辑更清晰,提高项目的可读性和维护性。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7Lga7FK-1579244723039)(/Users/zhangchao/Desktop/学习归档/RN学习笔记/Redux状态容器/images/rn重构.png)]

3. 使用bindActionCreators简化Action的分发

什么是bindActionCreators?

bindActionCreators作用是在使用redux的connect将react与redux store关联起来的connectmapDispatchToProps函数中将单个或多个Action Creator转化为dispatch(action)的函数集合形式。开发者不用再手动dispatch(actionCreator(type)),而是可以直接调用方法。

bindActionCreators原理

bindActionCreators实际上就是将dispatch直接和单个或多个action creator结合好然后发出去的这一部分操作给封装成一个函数。bindActionCreators 会使用dispatch将这个函数发送出去。

使用与不使用bindActionCreators对比

假如我们通过action creator来创建action

UserAction.js

//添加用户的同步action
export const addUser = (user) => {
    return {
        type: ADD_USER,
        user,
    };
};
//删除用户的同步action
export const removeUser = (user)=>{
    return {
        type: REMOVE_USER,
        user,
    };
}
//计算总用户数量
export const sumUser = () => {
    return {
        type: SUM_USER,
    };
};

然后在TestComponent.js组件中通过mapDispatchToProps函数中使用:

  • 不使用bindActionCreators

    //1.导入UserAction
    import {addUser,removeUser,sumUser} from './actions/UserAction';
    
    //2.定义connect的mapDispatchToProps函数
    const mapDispatchToProps = (dispatch)=>{
      return{
        propsAddUser:(user)=>{
          //通过dispatch来分发指定的Action
          dispatch(addUser(user))
        },
        propsRemoveUser:(user)=>{
          dispatch(removeUser(user))
        },
        propsSumUser:()=>{
          dispatch(sumUser())
        }
      }
    }
    //通过connect将react与Redux Store关联并导出组件
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //3.组件内调用mapDispatchToProps中定义的映射的props
    <Button title='添加用户' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中国上海'
         }
         //通过this.props.xxx 指定调用mapDispatchToProps中定义的属性即可。
         this.props.propsAaddUser(user);
         //this.props.propsRemoveUser(user);
         //this.props.propsSumUser();
    }}>
    
  • 使用bindActoinCreators

    格式:

    bindActionCreators(actionCreators,dispatch)

    参数:

    • actionCreators:(函数对象):也可以是一个对象,这个对象的所有元素都是action create函数。
    • dispatch:(功能):在Store实例dispatch上可用的功能。

    示例:

    //1.导入UserAction
    import {addUser,removeUser,sumUser} from './actions/UserAction';
    
    //2.导入redux中的bindActionCreators
    import {bindActionCreators} from 'redux';
    
    //3.定义connect的mapDispatchToProps函数
    const mapDispatchToProps = (dispatch)=>{
      return{
        /**
         *使用bindActionCreators将多个action creator转换成dispatch(action)形式,
         *此处不在手动调用disptch(action)了。
         */
         actions:bindActionCreators({
           propsAddUser:(user)=>addUser(user),
           propsRemoveUser:(user)=>removeUser(user),
           propsSumUser:sumUser,  //不带参数的action creator
         },dispatch),
      }
    }
    //通过connect将react与Redux Store关联并导出组件
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //4.组件内调用mapDispatchToProps中定义的映射的props
    <Button title='添加用户' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中国上海'
         }
         /**
          *通过this.props.actions.xxxx 指定调用mapDispatchToProps中
          *bindActionCreators定义的props即可。
          */
         this.props.actions.propsAddUser(user);
         //this.props.actions.propsRemoveUser(user);
         //this.props.actions.propsSumUser();
    }}>
    

    (推荐)通过import * as xxx的形式将一个文件中的所有action creator全部导入方式实现

    //1.导入UserAction中所有的action creator
    import * as UserActions from './actions/UserAction';
    
    //2.导入redux中的bindActionCreators
    import {bindActionCreators} from 'redux';
    
    //3.定义connect的mapDispatchToProps函数
    const mapDispatchToProps = (dispatch)=>{
      return {
        /**
         *通过bindActionCreators将UserAction.js中所有的action creator转换成dispatch(action)
         *形式,此处不在手动调用disptch(action)了。
         */
        actions:bindActionCreators(UserActions,dispatch);
      }
    }
    //通过connect将react与Redux Store关联
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //4.组件内调用mapDispatchToProps中定义的映射的props
    <Button title='添加用户' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中国上海'
         }
         //通过this.props.actions.xxxx 指定调用UserAction.js中具体的action即可。
         this.props.actions.addUser(user);
         //this.props.actions.removeUser(user);
         //this.props.actions.sumUser();
    }}>
    

对于异步Action如何使用bindActionCreator

在我们使用redux-thunk时候通过创建返回函数方式实现异步Action,那么在bindActionCreators中如何使用异步Action呢?其实与同步Action使用没有太大区别。如果异步Action调用的函数有返回值,并且通过bindActionCreator绑定次异步函数后,我们在通过this.props.xxxx (xxx为函数名)调用异步函数时候直接接受返回值。

异步Action(UserAction.js)

//通过创建返回函数方式创建Action
const addUser = (user)=>{
  return (dispatch)=>{
     //异步Action有返回值
     return await new Promise((resolve, reject) => {
            //模拟异步网络请求
            setTimeout(() => {
                dispatch(changeLoginBtnEnable(true));
                let userInfo = {
                        userId: 10001,
                        realName: 'zcmain',
                        address: '中国上海',
                    };
                 dispatch(updateUserInfoVo(userInfo));
                 resolve('success');
                }, 2000);
            });
       }
 }

bingActionCreators进行绑定异步Action

...
import {UserActions} from './actions/UserAction';

const mapDispatchToProps = (dispatch)=>{
  return{
    //通过bindActionCreatros将action creator转换成dispatch(action)
    actions:bindActionCreatros(UserActions,dispatch);
  }
}

组件中使用异步Action

...
<Button title='添加用户',onPress={()=>{
    let user={
      id:'1',
      name:'zcmain',
    }
    /**
     *通过bindActionCreator绑定后的action creator的调用函数如果有返回值,
     *通过this.props.属性调用时候直接接受返回
     */
    this.props.actions.addUser(user)
      .then((response)=>{
           //TOOD...
      		 console.log('成功:' + JSON.stringify(response);
    	},(error)=>{
      		 console.log('失败:' + error.message);
    	}).catch((exception)=>{
      		 console.log('异常:' + JSON.stringify(exception));
    });
}}/>

bindActionCreators源码解析

  1. 判断传入的参数是否是object,如果是函数,就直接返回一个包裹dispatch的函数;
  2. 如果是object,就根据相应的key,生成包裹dispatch的函数即可;
/**
 *bindActionCreators函数
 *参数说明:
 *@param actionCreators: action create函数,可以是一个单函数,也可以是一个对象,这个对象的所有元素
 *都是action create函数;
 *@param dispatch: store.dispatch方法;
 */
export default function bindActionCreators(actionCreators, dispatch) {
  /**
   *如果actionCreators是一个函数的话,就调用bindActionCreator方法对action create函数
   *和dispatch进行绑定。
   */
  if (typeof actionCreators === 'function') {
      return bindActionCreator(actionCreators, dispatch)
  }
  /**
   *如果actionCreators不是一个对象或者actionCreators为空,则报错
   */
  if (typeof actionCreators !== 'object' || actionCreators === null) {
      throw new Error('bindActionCreators expected an object or a function, + 
                      'instead received ${actionCreators === null ?+
                      'null' : typeof actionCreators}.' +
                      'Did you write "import ActionCreators from" instead of +
                      '"import * as ActionCreators from"?')
  }

  //否则actionCreators是一个对象获取所有action create函数的名字
  const keys = Object.keys(actionCreators)
  //遍历actionCreators数组对象保存dispatch和action create函数进行绑定之后的集合
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    // 排除值不是函数的action create
    if (typeof actionCreator === 'function') {
      // 进行绑定
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  //返回绑定之后的对象
 return boundActionCreators
}

/**
 *bindActionCreator函数
 */
function bindActionCreator(actionCreator, dispatch) {
  // 这个函数的主要作用就是返回一个函数,当我们调用返回的这个函数的时候,就会自动的dispatch对应的action
  // 这一块其实可以更改成如下这种形式更好
  // return function(...args) {return dispatch(actionCreator.apply(this, args))}
  return function() { return dispatch(actionCreator.apply(this, arguments)) }
}

高级

异步Action

前面我们将的action创建都是同步状态,当dispatch(action)时候,state会被立即更新。

创建同步Action(返回的是一个action对象):

//创建同步action
export const syncAddItem = {
  type:'addItem',
  text:'增加一条数据',
}
//或者通过函数创建(可接收参数,返回action)
export const syncAddItem=(desc)=>{
  return{
    type:'addItem',
    text:desc,
  }
}

//通过store触发同步action,State会立即被更新
store.dispatch(syncAddItem);
store.dispatch(syncAddItem('增加一条数据'));

对于异步action创建我们需要借助**Redux Thunk**中间件。 action创建函数除了返回action对象外还可以返回函数。这时这个action创建函数就成为了thunk

什么是(为什么使用)Redux Thunk?

我们之所以需要使用诸如 Redux-Thunk 之类的中间件,是因为 Redux 存储仅支持同步数据流。 于是,中间件来救援了! 中间件允许异步数据流,解释您分派的任何内容,并最终返回一个允许同步 Redux 数据流继续的普通对象。 因此,Redux 中间件可以解决许多关键的异步需求(例如 axios 请求)。

Redux Thunk 中间件允许您编写返回函数替代返回action对象。可以使用thunk中间件来进行延迟动作的分派,或者仅在满足某个条件时才分发。内部函数接收store的dispatchgetState作为参数。

当action创建函数返回函数时,这个函数会被Redux Thunk middleWare执行(如下创建的异步Action返回的return (dispatch)函数会被Thunk中间件执行),这个函数并不需要保持纯净;它可以带有副作用,包括执行异步API请求。这个函数还可以执行dispatch(action),就像dispatch同步的Action一样。

Redux Thunk在异步Action中使用

1. 创建异步Action(返回是一个函数会被Thunk中间件调用):

//创建一个异步的action,
export const asyncAction1 = (str) => {
    //返回一个接收dispatch参数的函数(该函数会被Thunk中间件调用),
    return (dispatch) => {
        //2秒后指定其他操作,比如触发dispatch(action)更新State
        setTimeout(() => {
            //dispatch(action);
            console.log(str);
        }, 2000);
    };
};

//storet通过dispatch方法分发异步Action
store.dispatch(asyncAction1('异步Action创建函数'))

当然异步Action返回函数除了接收dispatch参数外还可以接受getState参数,我们可以根据getState中的状态来进行逻辑判断执行不同的dispatch:

//创建一个异步的action,
export const asyncAction1 = (str) => {
    //返回一个接收dispatch和getState参数的函数(该函数会被Thunk中间件调用),
    return (dispatch,getState) => {
      //通过getState获取State中的counter属性,如果为偶数则返回不触发dispatch
       const {counter} = getState();
       if(counter % 2 === 0){
          return;
       }
       //否则2秒后指定其他操作,比如触发dispatch(action)更新State
       setTimeout(() => {
            //dispatch(action);
            console.log(str);
        }, 2000);
    };
};

//store调用dispatch方法
store.dispatch(asyncAction1('异步Action创建函数接收dispatch和getState属性'))

异步Action返回函数除了可以接收dispatchgetState两个参数以外,还可以通过Redux Thunk 使用withExtraArgument 函数注入自定义参数:

//通过Redux Thunk的withExtraArgument注入自定义参数到异步Action返回函数中
import {createStore,applyMiddleWare} from 'redux';
import thunk from 'redux-thunk';

//单个参数注入
const name ='zcmain';

//多个参数包装成对象注入
const age = 20,
const city = 'ShanHai',
const userInfo = {
  name,
  age
  city,
}

const store = createStore(
  reducer,
  //创建Store时候将自定的参数通过thunk.withExtraArgument注入到异步Action返回函数中
  applyMiddleware(thunk.withExtraArgument(name,userInfo)),
);
//异步Action
const asyncAction1 = ()=>{
  /**
   *返回函数接受三个参数,其中 name、userInfo 是通过
   *Thunk.withExtraArgument(name,userInfo)注入的自定义参数。
   */
  return (dispatch,getState,name,userInfo)=>{
       //TODO...you can use name and userInfo here
  }
}

Thunk middleware 中间件调用的函数可以有返回值,它会被当作 dispatch 方法的返回值传递。

//创建一个异步的Action
export const asyncAction2 = (url) => {
    //返回一个接收dispatch参数的函数
    return (dispatch) => {
        /**
         *thunk middleWare调用的函数返回一个Promise对象,它会被当作 dispatch 方法的返回值传递,
         *这里通过Fetch网络请求,响应结果后调用dispatch(action)来更新State。
         *注意:
         *  不要使用 catch,因为会捕获,在 dispatch 和渲染中出现的任何错误,
         *  导致 'Unexpected batch number' 错误。
         *  https://github.com/facebook/react/issues/6895
         */
        return fetch(url)
          .then((response) =>{
                 response.json()
               },(error)=>{
          
           })
          .then((json) => {
            //收到相应后发送dispatch(action)来更新State
            dispatch({type:'UPDATA',data:json});
            return json;
        });
    };
};

/**
 *store通过dispatch方法触发异步Action,因为该action返回函数有返回值,
 *会被当作是dispatch方法的返回值传递。
 */
store.dispatch(asyncAction2('http://xxx.xxx.xxx.xxx:xxx/test/json))
     .then((json)=>{
      //TODO...
     }).catch(()=>{
      //TODO...
});

注意:

Thunk middleWare执行有返回值的函数中不要使用 catch,因为会捕获,在 dispatch 和渲染中出现的任何错误, Unexpected batch number 错误

https://github.com/facebook/react/issues/6895

2. 使用异步Action

上面我们创建好了异步的Action,接下来我们需要通过Redux Thunk中间件来使用该异步Action

  1. 安装redux-thunk库:

    npm -i --save redux-thunk
    #或者
    yarn add redux-thunk
    
  2. 通过Redux的applyMiddleWare使用Redux Thunk来创建store:

    AppStore.js

    //引入createStore、applyMiddleWare
    import {createStore,applyMiddleWare} from 'redux';
    //引入thunk中间件
    import thunk from 'redux-thunk';
    //引入reducers
    import reducers from '../reducers/AppReducers';
    
    //指定reducer和中间件来创建store
    const store = createStore(reducers,applyMiddleWare(thunk));
    
    //导出store
    export default store;
    
  3. 其他组件使用

    LoginComponent.js

    /**
     *登陆页面的mapDispatchToProps函数中使用dispatch方法触发异步的action,会在两秒后打印日志;
     *注意:mapDispatchToProps函数要依赖react-redux的Provider和connect。
     */
    const mapDispatchToProps = (dispatch) => {
        return {
            onclick: () => {
                dispatch(LoginAction.asyncAction1('发送异步action'));
            },
        };
    };
    
    //两秒后会打印
    LOG  str:发送异步Actoin啦...
    
    /**
     *dispatch分发有有返回值的异步asyncAction2
     */
    const mapDispatchToProps = (dispatch) => {
        return {
            onclick: () => {
                dispatch(LoginAction.asyncAction2('发送异步action接受返回值'))
                 .then((json)=>{
                     console.log('dispatch 分发异步Action并接收返回值:' + json)
                 });
            },
        };
    };
    

范式化数据

为什么要设置范式化state数据结构?

事实上,大部分程序处理的数据都是嵌套或互相关联的,那么如何在state中使用嵌套及重复的数据(对象复用)?例如:

const blogPosts = [
    {
        id : "post1",
        author : {username : "user1", name : "User 1"},
        body : "......",
        comments : [
            {
                id : "comment1",
                author : {username : "user2", name : "User 2"},
                comment : ".....",
            },
            {
                id : "comment2",
                author : {username : "user3", name : "User 3"},
                comment : ".....",
            }
        ]    
    },
    {
        id : "post2",
        author : {username : "user2", name : "User 2"},
        body : "......",
        comments : [
            {
                id : "comment3",
                author : {username : "user3", name : "User 3"},
                comment : ".....",
            },
            {
                id : "comment4",
                author : {username : "user1", name : "User 1"},
                comment : ".....",
            },
            {
                id : "comment5",
                author : {username : "user3", name : "User 3"},
                comment : ".....",
            }
        ]    
    }
    // and repeat many times
]

上面的数据结构比较复杂,并且有部分数据是重复的。这里还存在一些让人关心的问题:

  • 难以保证所有复用的数据同时更新:当数据在多处冗余后,需要更新时,很难保证所有的数据都进行更新。
  • 嵌套复杂度高:嵌套的数据意味着 reducer 逻辑嵌套更多、复杂度更高。尤其是在打算更新深层嵌套数据时。
  • 不可变的数据在更新时需要状态树的祖先数据进行复制和更新,并且新的对象引用会导致与之 connect 的所有 UI 组件都重复 render。尽管要显示的数据没有发生任何改变,对深层嵌套的数据对象进行更新也会强制完全无关的 UI 组件重复 render。

正因为如此,在 Redux Store 中管理关系数据或嵌套数据的推荐做法是将这一部分视为数据库,并且将数据按范式化存储。

设计范式化State数据结构

范式化结构包含以下几个方面:

  1. 任何类型的数据在state中都有自己的"表";
  2. 任何 “数据表” 应将各个项目存储在对象中,其中每个项目的 ID 作为 key,项目本身作为 value
  3. 任何对单个项目的引用都应该根据存储项目的 ID来完成。
  4. ID 数组应该用于排序。

上面博客示例中的 state 结构范式化之后可能如下:

authorcomments对象提取出来通过byId来使用

{
    posts : {
        byId : {
            "post1" : {
                id : "post1",
                author : "user1",
                body : "......",
                comments : ["comment1", "comment2"]    
            },
            "post2" : {
                id : "post2",
                author : "user2",
                body : "......",
                comments : ["comment3", "comment4", "comment5"]    
            }
        }
        allIds : ["post1", "post2"]
    },
    comments : {
        byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment : ".....",
            },
            "comment3" : {
                id : "comment3",
                author : "user3",
                comment : ".....",
            },
            "comment4" : {
                id : "comment4",
                author : "user1",
                comment : ".....",
            },
            "comment5" : {
                id : "comment5",
                author : "user3",
                comment : ".....",
            },
        },
        allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
    },
    users : {
        byId : {
            "user1" : {
                username : "user1",
                name : "User 1",
            }
            "user2" : {
                username : "user2",
                name : "User 2",
            }
            "user3" : {
                username : "user3",
                name : "User 3",
            }
        },
        allIds : ["user1", "user2", "user3"]
    }
}
表间关系

因为我们将Redux Store视为数据库,所以在很多数据库设计规则里面也是同样适用的。例如:对于多对多的关系,可以设计一张中间表用于存储相关联的项目ID(经常被称为相关表或者关联表)。为了一致性起见,我们还会使用相同的byIdallIds用于实际的数据项表中。

entities:{
  authors:{byId:{},allIds:[]},
  book:{byId:{},allIds:[]},
  authorBook:{
    byId:{
      1:{
        id : 1,
        authorId : 5,
        bookId : 22
      },
      2:{
        id : 2,
        authorId : 5,
        bookId : 15,
      }
    },
    allIds:[1,2]
  }  
}
嵌套数据范式化

因为 API 经常以嵌套的形式发送返回数据,所以该数据需要在引入状态树之前转化为规范化形态。Normalizr 库可以帮助你实现这个。你可以定义 schema 的类型和关系,将 schema 和响应数据提供给 Normalizr,他会输出响应数据的范式化变换。输出可以放在 action 中,用于 store 的更新。有关其用法的更多详细信息,请参阅 Normalizr 文档。

管理范式化数据

当数据存在ID、嵌套或者关联关系时,应当以范式化形式存储:对象只能存储一次,ID作为键值,对象间通过ID相互引用。

将Store类比于数据库,每一项都是独立的"表"。normalizrredux-orm此类的库能在管理规范化数据时提供参考和抽象。

中间件使用

redux-thunk

Redux Thunk是Redux中提供异步Action处理的中间件,具体使用参考上面文章《什么是Redux Thunk

redux-saga

redux-saga也是用于解决RN中异步交互的问题,与redux-thunk目标一致,不同点在于:

  • redux-thunk:

    • 介绍:是redux推出一个MiddleWare,使用简单,允许action 创建函数除了返回 action 对象外还可以返回函数,并且该返回函数可以接受dispatchgetState作为参数。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action。
    • 优点:代码量小,上手简单适合轻小型应用程序中。
    • 缺点:返回函数内部复杂,不易维护。由于thunk使得Action创建函数返回不再是一个action对象,而是一个函数,而函数的内部可以多种多样,甚至更为复杂,显然使得action不易于维护。
  • redux-saga

    • 介绍:官网上的描述redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单
    • 优点:避免回调地狱(当前thunk 使用async/await也可以解决),方便测试和维护,适合大型应用程序。
    • 缺点:陡峭学习路线,样板代码量大。

    在许多正常情况下和中小型应用程序中,使用async / await风格redux-thunk。它可以为你节省很多样板代码/操作/类型,而且你不需要在很多不同的sagas.ts之间切换,也不需要维护-一个特定的sagas树。但是,如果你正在开发一个大型的应用程序,其中包含非常复杂的异步,并且需要一些特性,比如并发/并行模式,或者对测试和维护有很高的需求(尤其是在测试驱动开发中),那么redux -sagas可能会拯救你的生命。

    参见《Redux-Thunk vs. Redux-Saga

redux-ignore

redux-ignore可以指定reducer函数触发条件(例如:指定某个/某些actions才会触发当前reducer函数)。

对于通过combineReducers合并拆分的Reducer来说,触发每个 action 都会调用 所有的 reducer,JavaScript 引擎有足够的能力在每秒运行大量的函数调用,而且大部分的子 reducer 只是使用 switch 语句,并且针对大部分 action 返回的都是默认的 state。如果你仍然关心 reducer 的性能,可以使用类似 redux-ignore工具,确保只有某些action会调用一个 reducer 或几个reducer。

  • 安装redux-ignore

    npm -i --save redux-ignore
    #或者
    yarn add redux-ignore
    
  • 配置combineReducer组合Reducer,并通过filterActions(也可通过ignoreActions忽略指定的action)指定能够触发Reducer执行的action

    import {combineReducers} from 'redux';
    import {filterActions} from 'redux-ignore/src';
    import {reducerA} from './ReducerA';
    import {reducerB} from './ReducerB';
    
    //通过combineReducers将分散的Reducer组合成一个reducers
    const reducers = combineReducers(
        reducerA:fileterAction(
      			    reducerA,
                    /**
                     *指定能够触发reducerA执行的Action数组,只有dispatch该数组内的action,
                     *reducerA才会调用。
                     */
      			     [
      					'actionA1',       
      					'actionA2'
      					 ...
      				 ]),
      
        reducerB:fileterAction(
          		    reducerB,
          		    /**
                     *指定能够触发reducerB执行的Action数组,只有dispatch该数组内的action,
                     *reducerA才会调用。
                     */
          			[
                        'actionB1',
                        'actionB2'
                         ...
                    ]);
    )
    
reselect

什么是reselect?

reselect可以作为 redux 的一个中间件,它通过传入的多个state计算获得新的 state,然后传递到 Redux Store。其主要就是进行了中间的那一步计算,使得计算的状态被缓存,从而根据传入的 state 判断是否需要调用计算函数(selectTodos),而不用在组件每次更新的时候都进行调用,从而更加高效。

在Redux中使用用于优化mapStateToProps中需要大量计算的业务逻辑

安装Reselect

npm -i --save reselect
#或者
yarn add reselect

我们来看一个connect中的mapStateToProps函数

// 一个 state 计算函数(假如内部有很大的计算量)
export const selectTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(todo => todo.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(todo => !todo.completed)
    ...
    ...
  }
}

/**
 *mapStateToProps 就是一个 selector,每次State变化时候就会被调用。
 *【缺点】每次组件更新的时候都会执行selectTodos函数重新计算visibleTodos,如果计算量比较大,
 * 会造成性能问题。
 */
const mapStateToProps = (state) => {
    return{
        //调用selectTodos函数根据条件计算todos
        visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    }
};

之前 connect 函数实现的时候,我们知道映射 props 的函数被 store.subscribe(),因此每次组件更新的时候,无论 state 是否改变,都会调用 mapStateToProps,而 mapStateToProps 在计算 state 的时候就会调用 state 计算函数selectTodos,过程 如下:

store.subscribe()(注册事件) —>状态更新时调用 mapStateToProps(一个selector,返回 state) —> 调用 state 计算函数 selectTodos

那么,问题来了,如果 selector 的计算量比较大,每次更新的重新计算就会造成性能问题。

而解决性能问题的 出发点 就是:避免不必要的计算

解决问题的方式:从 selector 着手,即 mapStateToProps,如果 selector 接受的状态参数不变,那么就不调用计算函数,直接利用之前的结果。

Reselect 提供 createSelector 函数来创建可记忆的 selector。

createSelector函数原型:

createSelector(…inputSelectors|[inputSelectors],resultFunc)

该函数接受一个或者多个selectors,或者一个selectors数组,计算他们的值并且作为参数传递给resultFunc,

createSelector通过判断input-selector之前调用和之后调用的返回值是否全等于来觉得是否调用resultFunc

  • 第一个参数:多个inputSelector或一个inputSelector数组
  • 第二个参数:转换函数

注意:

  1. 如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。
  2. 如果 input-selectors 的值和前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。

因此我们可以将之前的state 计算函数selectTodos放在createSelector函数的第二个参数转换函数内部,只有state改变引起input-selector值变化才会调用转换函数重新进行state计算,否则直接使用之前的结果,避免了再一次进行state值计算。

使用Reselect

我们通过使用reselect对上面的代码state计算进行优化

//导入createSelector函数
import {createSelector} from 'reselect';

//定义getTodos input-selector(接收参数state)
const getTodos = (state)=>{
  return state.todos;
};

//定义getVisibilityFilter input-selector(接收参数state)
const getVisibilityFilter = (state){
  return state.visibilityFilter
};

/**
 *一个 state 计算函数(假如内部有很大的计算量)。
 *使用createSelector函数优化:根据input-selector创建记忆的Selector。
 *将计算逻辑放在转换函数中。
 */
const selectTodos =createSelector(
      //第一个参数是input-selector数组
  		[getTodos,getVisibilityFilter],
      //第二个桉树为转换函数,仅当state变更引起input-selector改变才会触发(内部执行大量计算)
      (todos,visibilityFilter)=>{
        switch (visibilityFilter) {
          case 'SHOW_ALL':
            return todos
          case 'SHOW_COMPLETED':
            return todos.filter(todo => todo.completed) //过滤todos数组中已完成的todos
          case 'SHOW_ACTIVE':
            return todos.filter(todo => !todo.completed)//过滤todos数组中未完成的todos
        }
      }
);

我们使用react-redux可以在mapStateToProps()中当作正常函数来调用可记忆的selector

/**
 *mapStateToProps 中调用记忆的selector函数selectTodos
 *当state的变化引起input-selector值改变的时候才会触发createSelector中转换函数的执行进行计算,
 *否则跳过计算直接使用缓存数据。
 */
const mapStateToProps = (state) => {
   return{
      //调用可记忆的selector(参数依据input-selector接收的参数类型来定)
      visibleTodos: selectTodos(state),   
   }
};

在上例中, getTodosgetVisibilityFilter 都是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。

但是,selectTodos 是一个可记忆的 selector。他接收getTodosgetVisibilityFilter 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。这样当state的变化引起input-selector值改变的时候才会触发createSelector中转换函数的执行进行计算,否则跳过计算直接使用缓存数据,避免不必要的计算。

组合Selector

上面我们指定input-selector和转换函数通过createSelector创建了可记忆的Selector(selectTodos)。同时可记忆的Selector自身也可作为其它可记忆的selector的input-selector。我们把上面可记忆的selectTodos 当作另一个可记忆selector的input-selector,来进一步通过关键字(keyword)过滤todos

//创建input-selector
const keyword = (state)=>{
  return state.keyword;
}
//创建组合Selector
const getVisibleTodosFilterByKeyword = createSelector(
      /**
       *第一个参数input-selector数组:
       *selectTodos:上面创建可记忆的selector。
       *keyword :本次创建的input-selector。
       */
  		[selectTodos,keyword],
      /**
       *第二个参数:转换函数,
       *参数位:input-selector数组中返回值
       */
      (visibleTodos,keyword)=>{
        return visibleTodos.filter((todo)=>{
          //指定关键字通过对可记忆的selectTodos筛选的结果进一步筛选后返回。
          todo.text.indexOf(keyword)>1
        });
      }
);

/**
 *mapStateToProps 中调用记忆的组合selector函数getVisibleTodosFilterByKeyword
 */
const mapStateToProps = (state) => {
    return{
        visibleTodos: getVisibleTodosFilterByKeyword(state),
    }
};

在Selectors中访问React props

到目前为止,我们只看到selector接收Redux store state作为参数,然而,selector也可以接收props。

//定义todos input-selector
const getTodos = (state,props)=>{
  //you can user props here
  return state.todos;
};

//定义 filter input-selector
const getVisibilityFilter = (state,props){
  //you can user props here
  return state.visibilityFilter
};

//创建可记忆的selector,同上代码一样不变,省略...
const selectTodos = createSelector(
  		[getTodos,getVisibilityFilter],
      (todos,visibleFilter)=>{
         //TODO...
      }
);

mapStateToProps()中调用将props传递给可记忆的selector selectTodos()函数

const mapStateToProps = (state,props)=>{
   return{
      //调用可记忆的selector将state和props传递过去
      visibleTodos: selectTodos(state,props),
   }
}

注意:

使用createSelector创建的selector只有在参数集与之前的参数集相同时才会返回缓存值。

例如一个组件A中多次使用同一个组件B,但是多个B组件中的属性props不一样,就会导致组件B中的selector无效,因为同一个组件但是参数集props不一样了,会导致B组件selector重新计算)。

//组件A
export default A extends Component{
  render(){
    return(
      <View>
       /**
        *多次使用组件B,但是每个B组件中index属性值都不同,这会导致B组件中的selector无效。
        *每次执行B组件都输导致selector重新计算。
        */
       <B index=1/>
       <B index=2/>
       <B index=3/>
      </View>
    );
  }
}

同个组件多个实例的共享Selector

上面我们说了createSelector创建的selector只有在参数集与之前参数集相同才会返回缓存值,那么我们想要在一个组件多个实例中共享selector该如何实现呢?

例如我们在A组件中使用B组件的多个实例,如何让这些B组件实例共享selector呢?

解决方案:

组件的各个实例需要他们自己的selector备份。

实现方案:

  1. 创建一个函数,这个函数每次调用的时候返回一个新的selector

    /**
     *1.定义input-selector
     */
    const getTodos = (state)=>{
      return state.todos;
    }
    
    const getVisibleFilter = (state)=>{
      return state.visibleFilter;
    }
    
    /**
     *2.创建可记忆的selector
     *旧方案:
     *		此时的selector直接可被mapStateToProps调用了,缺点是同一个组件不同的实例
     *		如果属性(参数值)不同,都会触发selector重新计算。
     *优化:
     *		该selector不直接让mapStateToProps调用,而是通过一个函数返回。
     */
    const selectTodos = createSelector(
    	  [getTodos,getVisibleFilter],
          (todos,visibilityFilter)=>{
            switch (filter) {
              case 'SHOW_ALL':
                return todos
              case 'SHOW_COMPLETED':
                return todos.filter(todo => todo.completed) //过滤todos数组中已完成的todos
              case 'SHOW_ACTIVE':
                return todos.filter(todo => !todo.completed)//过滤todos数组中未完成的todos
            }
        }
    );
    
    /**
     *3.创建一个函数,返回可记忆的selector
     */
    const makeSelectTodos = ()=>{
      return selectTodos();
    }
    
  2. 给组件实例设置各自获取私有的selector的方法。mapStateToProps函数可以实现。

    知识点

    • 如果connect函数的mapStateToProps返回的不是一个对象而是是一个函数,他就可以被用来为每个组件的容器创建一个私有的mapStateProps函数。如下:
    /**
     *旧的方案:
     *mapStateToProps函数直接返回了对象,并且对象内部直接调用可记忆的selector函数,
     *缺点是不同实例的属性不同,会导致selector失效,每次调用都会导致selector重新计算。
     */
    const mapStateToProps = ()=>{
      return{
         //直接调用可记忆的selectTodos函数
          visibleTodos: selectTodos(state,props),
      }
    }
    
    
    /**
     *优化:
     *创建一个函数,内部调用上一步中创建的返回可记忆的selector的函数来获取各自实例私有的selector。
     *然后该函数返回一个mapStateToProps函数(mapStateToProps函数中调用私有的selector)
     */
    const makeMapStateToProps =()=>{
      //获取实例私有的selector
      const getSelectTodos = makeSelectTodos();
      //创建mapStateToProps函数,内部调用私有的selector
      const mapStateToProps = (state,props)=>{
        return{
          //调用实例私有的selector函数
           visibleTodos: getSelectTodos(state,props),
        }
      }
      //返回这个mapStateToProps函数
      return mapStateToProps;
    }
    
  3. 最后将这个makeMapStateToProps传递到connect中,那么组件容器的每一个实例中将会获得各自含有私有的selector的mapStateToProps的函数。

    export default connect(makeMapStateToProps,mapDispatchToProps)(XXXX);
    
redux-router
redux-promise

RN项目目录结构

目前的项目比较简单结构RN部分如下:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ZuOFbBu-1579244723042)(/Users/zhangchao/Desktop/学习归档/RN学习笔记/Redux状态容器/images/目录结构.png)]

  • common:主要存放一些公用的模块。比如对话框、导航栏、样式表、自定义的小组件等。
    在这里插入图片描述

  • constant:存放常量。比如应用常量和配置信息。
    在这里插入图片描述

  • page:存放UI相关的组件和实现
    在这里插入图片描述
    注意:项目中使用了react-redux和一些MiddleWare,因此将UI实现相关的逻辑统一放在业务模块下以便于快速定位和排查问题。例如login模块下:

    • LoginAction:业务模块所需的Action

      负责创建业务所需的Action和Action Creator(异步Action业务逻辑处理)。

    • LoginComponent:业务的展示组件

      仅负责UI的渲染。

    • LoginContainer:业务容器组件

      负责将展示组件与Redux Store通过**connect**函数关联,并将Store state和dispatch映射到展示组件Props中。可通过在mapDispatchToProps()函数中使用bindActionCreators()函数来简化dispatch(action)分发。

    • LoginReducer:业务模块reducer函数。

      负责根据Action类型更新Store State。

  • redux:存放redux涉及的actionType、经过combineReducers函数合并的reducer、Redux Store:
    在这里插入图片描述

  • utils:存放一些工具类。比如数据持久化存储、字符编码、哈希散列、加解密、网络请求等工具类。
    在这里插入图片描述

参考文献

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值