基础
什么是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.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的方法。
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为定义返回初始化的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; } }
注意:
-
不要修改
state
使用
Object.assign()
新建了一个副本。不要使用下面方式Object.assign(state,{visibilityFilter:action.filter})
,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。 -
在
default
情况下返回旧的state
。遇到未知的action时,一定要返回旧的state
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。 -
根据已有的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:{}, //数据域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建立映射。
注意:
- 使用
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默认使用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源码解析
- 判断传入的参数是否是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 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的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)更新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返回函数除了可以接收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,
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
-
安装
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-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 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数组;
- 第二个参数:转换函数;
注意:
- 如果 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 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),
}
};
在上例中, 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 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备份。
实现方案:
-
创建一个函数,这个函数每次调用的时候返回一个新的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(); }
-
给组件实例设置各自获取私有的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; }
- 如果connect函数的
-
最后将这个makeMapStateToProps传递到connect中,那么组件容器的每一个实例中将会获得各自含有私有的selector的mapStateToProps的函数。
export default connect(makeMapStateToProps,mapDispatchToProps)(XXXX);
redux-router
redux-promise
RN项目目录结构
目前的项目比较简单结构RN部分如下:
-
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:存放一些工具类。比如数据持久化存储、字符编码、哈希散列、加解密、网络请求等工具类。