文章目錄
基礎
什麼是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:存放一些工具類。比如數據持久化存儲、字符編碼、哈希散列、加解密、網絡請求等工具類。