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
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的方法。
action和action创建函数这两个概念很容易混在一起,使用时最好注意区分。在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请求、没有变量修改,单纯执行计算。 - reducer 第一次被调用的时候,
-
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;} }注意:
-
不要修改
state使用
Object.assign()新建了一个副本。不要使用下面方式Object.assign(state,{visibilityFilter:action.filter}),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。 -
在
default情况下返回旧的state。遇到未知的action时,一定要返回旧的stateObject.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) -
发起Action(
index.js)经过上述几个步骤,我们已经创建了
action、reducer、store了,此时我们可以验证一下,虽然没有页面,因为它们都是纯函数,只需要调用一下,对返回值做判断即可。写测试就这么简单。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建立映射。
注意:
- 使用
connect()前,需要先定义mapStateToProps; - 使用connect连接的组件需要被
Provider组件包裹; mapStateToProps:这个函数来指定将当前组件Props与 Redux store state 建立映射关系。在每次 store 的 state 发生变化的时候,应用内所有组件的该函数都会被调用。如果不传组件不会监听Store State的变化,也就是说Store的更新不会引起UI的更新。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中)
-
定义Action 类型
//ActionType.js //登陆状态 const LOGIN_TYPE = {LOGIN_SUCCESS: 'LOGIN_SUCCESS',LOGIN_FAILED: 'LOGIN_FAILED',LOGIN_WAITING: 'LOGIN_WAITING', }; export {LOGIN_TYPE}; -
创建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: '登陆失败...', }; -
创建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}; -
创建Redux Store
//store.js import {createStore} from "redux"; import {AppReducer} from '../reducers/AppReducer'; //依据Reducer创建store const store = createStore(AppReducer); export default store; -
使用入口
//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中内容。 -
子组件中访问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使其各司其职处理相关的业务。
-
拆分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: '注册失败!',}, }; -
拆分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源码解析
- 判断传入的参数是否是object,如果是函数,就直接返回一个包裹dispatch的函数;
- 如果是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的dispatch和getState作为参数。
当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返回函数除了可以接收dispatch和getState两个参数以外,还可以通过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
-
安装
redux-thunk库:npm -i --save redux-thunk #或者 yarn add redux-thunk -
通过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; -
其他组件使用
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数据结构
范式化结构包含以下几个方面:
- 任何类型的数据在state中都有自己的"表";
- 任何 “数据表” 应将各个项目
存储在对象中,其中每个项目的 ID 作为 key,项目本身作为 value。 - 任何对单个项目的
引用都应该根据存储项目的 ID来完成。 - ID 数组应该用于排序。
上面博客示例中的 state 结构范式化之后可能如下:
将author和comments对象提取出来通过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(经常被称为相关表或者关联表)。为了一致性起见,我们还会使用相同的byId和allIds用于实际的数据项表中。
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 对象外还可以
返回函数,并且该返回函数可以接受dispatch、getState作为参数。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action。 - 优点:代码量小,上手简单适合轻小型应用程序中。
- 缺点:返回函数内部复杂,不易维护。由于thunk使得Action创建函数返回不再是一个action对象,而是一个函数,而函数的内部可以多种多样,甚至更为复杂,显然使得action不易于维护。
- 介绍:是redux推出一个MiddleWare,使用简单,允许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执行的actionimport {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数组;
- 第二个参数:转换函数;
注意:
- 如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。
- 如果 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), }
};
在上例中, getTodos 和getVisibilityFilter 都是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。
但是,selectTodos 是一个可记忆的 selector。他接收getTodos和 getVisibilityFilter 为 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备份。
实现方案:
-
创建一个函数,这个函数每次调用的时候返回一个新的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(); } -
给组件实例设置各自获取私有的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; } - 如果connect函数的
-
最后将这个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)]](https://img-blog.csdnimg.cn/20200117151552202.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 常见问题
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
