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

文章目录

    • 基础
      • 什么是Redux?
      • 安装Redux
      • 核心思想
      • 三大原则
        • 单一数据源
        • State只读
        • 使用纯函数来执行修改
      • Action
      • Reducer
      • Store
      • State的基本结构
      • `React-Redux` 使用
        • 安装React Redux
        • 核心API讲解
          • 1. Provider
          • 2. connect
        • 完整示例代码
        • 扩展:
          • 1. 嵌套组件中访问Redux Store State
          • 2. 使用`combineReducers`合并多个零散Reducer
          • 3. 使用`bindActionCreators`简化Action的分发
    • 高级
      • 异步Action
        • 什么是(为什么使用)Redux Thunk?
        • Redux Thunk在异步Action中使用
      • 范式化数据
        • 为什么要设置范式化state数据结构?
        • 设计范式化State数据结构
        • 表间关系
        • 嵌套数据范式化
        • 管理范式化数据
      • 中间件使用
        • redux-thunk
        • redux-saga
        • redux-ignore
        • reselect
        • redux-router
        • redux-promise
    • RN项目目录结构
      • 参考文献

基础

什么是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.filterdefault: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为定义返回初始化的stateif(typeof state === 'undefined'){return initialState;}//这里暂不处理任何action,仅返回传入的statereturn state
    }
    

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

    function todoApp(state = initialState,action){//这里暂不处理任何action,仅返回传入的statereturn 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.filterdefault: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:{},   //数据域1domainData2:{},   //数据域2appState1:{},     //应用状态域1appState2:{},     //应用状态域1ui:{              //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默认使用LoginReducerregisterReducer: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来分发指定的Actiondispatch(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 createif (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)更新StatesetTimeout(() => {//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属性,如果为偶数则返回不触发dispatchconst {counter} = getState();if(counter % 2 === 0){return;}//否则2秒后指定其他操作,比如触发dispatch(action)更新StatesetTimeout(() => {//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,agecity,
}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)来更新Statedispatch({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类比于数据库,每一项都是独立的"表"。normalizr、redux-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 todoscase '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函数根据条件计算todosvisibleTodos: 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 todoscase 'SHOW_COMPLETED':return todos.filter(todo => todo.completed) //过滤todos数组中已完成的todoscase '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 herereturn state.todos;
};//定义 filter input-selector
const getVisibilityFilter = (state,props){//you can user props herereturn 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 todoscase 'SHOW_COMPLETED':return todos.filter(todo => todo.completed) //过滤todos数组中已完成的todoscase '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 =()=>{//获取实例私有的selectorconst getSelectTodos = makeSelectTodos();//创建mapStateToProps函数,内部调用私有的selectorconst 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:存放一些工具类。比如数据持久化存储、字符编码、哈希散列、加解密、网络请求等工具类。
    在这里插入图片描述

参考文献

  • Redux 中文文档
  • Redux 常见问题


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部