JS狀態容器—Redux與React-Redux及中間件使用

基礎

什麼是Redux?

Redux是JavaScript狀態容器,提供可預測化的狀態管理。可以讓你構建一致化的應用,運行於不同的環境。
Redux工作流向圖
Redux工作流向圖

安裝Redux

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

核心思想

Redux核心思想是通過action來更新state。

Action就像是描述發生了什麼的指示器。最終爲了把action和state串起來,開發一些函數,這些函數叫做reducer。reducer只是一個接收state和action並返回新的state的函數。

對於大應用來說,不大可能僅僅只寫一個這樣的函數,所以我們編寫很多小函數來分別管理state的一部分,最後通過一個大的函數調用這些小函數,進而管理整個應用的state。

三大原則

單一數據源

整個應用的state被存儲在一棵object tree中,並且這個object tree只存在於唯一一個store中。

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

State只讀

唯一改變state的方法就是觸發action,action是一個用於描述已發生事件的普通對象。

這樣確保了視圖和網絡請求都不能直接修改state,相反它們只能表達想要修改的意圖。因爲所有的修改都被集中化處理,且嚴格按照一個接一個順序執行,因此不用擔心競態條件的出現。

Action就是普通對象而已,因此它們可以被日誌打印、序列化、存儲、後期調試或測試回放出來。

//定義Action對象,並通過store.dispatch方法觸發Action
store.dispatch({
  type:'COMPLETE_TODO',
  index:1
})

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

使用純函數來執行修改

爲了描述action如何改變 state tree,你需要編寫reducers函數

Reducer只是一些純函數,它接收先前的state和action,並返回新的state。

剛開始你可能只有一個reducer,隨着應用的變大,你可以把它拆成多個小的reducers,分別獨立的操作state tree的不同部分,因爲reducers只是函數,你可以控制它們被調用的順序,傳入附加數據,甚至編寫可複用的reducer函數來處理一些通用任務。

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


//2. 定義多個小的reducer函數處理特定的state(參數爲先前的state和action)
function visibilityFilte(state = 'SHOW_ALL' , action){
  switch(action.type){
    case 'SET_VISIBBILITY_FILTER':
      return action.filter
    default:
      return state
  }
 }

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

//3. 通過combineReducers函數將多個小的reducers函數組合。
let reducer = combineReducers({visibilityFilte,todos})

//4. 通過reducer函數創建Redux Store對象來存放應用狀態
let store = createStore(reducer);

//5. 可以手動訂閱更新,也可以事件綁定到視圖層
store.subscribe(()=>{
  //當state更新會觸發這裏
  console.log(store.getState());
});

//6. 通過store指定action來觸發Action改變state,
store.dispatch({
  type:'COMPLETE_TODO',
  index:1
})

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

Action

  • 簡介

    Action 是把數據從應用(這裏之所以不叫View是因爲這些數據有可能是從服務器響應,用戶輸入或其他非View的數據)傳到store的有效載荷。它是store數據的唯一來源。一般來說你會通過store.dispatch()將action傳到store。(簡單說:action用於描述發生了什麼)

    添加新的todo任務的action是這樣的:

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

    Action本質是JavaScript的普通對象。我們約定,action內必須使用一個字符串類型的type字段來表示將要執行的動作。多數情況下,type會被定義成字符串常量。當應用規模越來越大時,建議使用獨立的模塊或文件存放action。

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

    除了type字段外,action對象的結構完全由你自己決定,參照 Flux 標準 Action 獲取關於如何構造 action 的建議。

    這時,我們還需要再添加一個action index來表示用戶完成任務的動作序列號。因爲數據存放在數組中的,所以我們通過下標Index來引用特定的任務。而實際項目中一般會在新建數據的時候生成唯一的ID作爲數據的引用標識。

    {
      type:TODO_ADD,
      index:5
    }
    

    我們應該儘量減少在action中傳遞數據。比如上面的例子,傳遞index就比把整個任務對象傳過去要好

  • Action創建函數(Action Creator)

    Action創建函數就是生成action的方法。actionaction創建函數這兩個概念很容易混在一起,使用時最好注意區分。

    在Redux中的action創建函數只是簡單的返回一個action:

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

    這樣做將使得action創建的函數更容易被移植和測試。

    store裏能直接通過store.dispatch()調用dispatch()方法,但是多數情況下你會使用react-redux提供的connect()幫助器來調用。bindActionCreators()可以自動把多個action創建的函數綁定到dispatch()方法上。

    注意:我們通常使用此中方式(action的工廠函數/action creator)構造action對象)

  • 源碼案例actions.js

    //action 類型(大型項目一般會獨立在一個組件中聲明,然用導出供其他組件使用)
    export const ADD_TODO = 'ADD_TODO';
    export const TOGGLE_TODO = 'TOGGLE_TODO';
    export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
    
    //其他常量對象
    export const VisibilityFilters = {
      SHOW_ALL:'SHOW_ALL',
      SHOW_COMPLETED:'SHOW_COMPLETED',
      SHOW_ACTIVE:'SHOW_ACTIVE'
    }
    
    //創建action函數並導出,返回action
    export function addTodo(text){
      return {
        type:ADD_TODO,
        text
      }
    }
    
    export function toggleTodo(index){
      return {
        type:TOGGLE_TODO,
        index
      }
    }
    
    export function setVisibilityFilter(filter){
      return {
        type:SET_VISIBILITY_FILTER,
        filter
      }
    }
    

Reducer

  • 簡介

    Reducers指定了應用狀態如何響應actions併發送到store的,記住actions只是描述了有事情發生這一事實,並沒有描述應用如何更新state。(簡單說:reducer根據action更新state)

    整個應用只有一個單一的 reducer 函數:這個函數是傳給 createStore 的第一個參數。一個單一的 reducer 最終需要做以下幾件事:

    • reducer 第一次被調用的時候,state 的值是 undefined。reducer 需要在 action 傳入之前提供一個默認的 state 來處理這種情況。
    • reducer 需要先前的 state 和 dispatch 的 action 來決定需要做什麼事。
    • 假設需要更改數據,應該用更新後的數據創建新的對象或數組並返回它們。
    • 如果沒有什麼更改,應該返回當前存在的 state 本身。

    注意:保持reducer純淨非常重要,永遠不要在reducer裏做這些操作

    • 修改傳入參數
    • 執行有副作用的操作,例如:請求和路由跳轉;
    • 調用非純淨函數,如:Date.now()Math.random()

    只需要謹記reducer一定要保持純淨。只要傳入參數相同,返回計算得到的下一個state就一定相同。沒有特殊情況、沒有副作用、沒有API請求、沒有變量修改,單純執行計算。

  • Action處理

    Redux首次執行時,state爲undefined,此時我們可藉機設置並返回應用的初始state。

    //引入 VisibilityFilters 常量對象
    import {VisibilityFilters} from './actions'
    
    //初始化state狀態。
    const initialState={
      visibilityFilter:VisibilityFilters.SHOW_ALL,
      todos:[]
    };
    
    //定義reducer函數
    function todoApp(state,action){
      //如果state爲定義返回初始化的state
      if(typeof state === 'undefined'){
           return initialState;
         }
       //這裏暫不處理任何action,僅返回傳入的state
      return state
    }
    

    使用ES6參數默認值語法精簡代碼

    function todoApp(state = initialState,action){
        //這裏暫不處理任何action,僅返回傳入的state
        return state;
    }
    

    現在可以處理action.type SET_VISIBILITY_FILTER。需做的只是改變state中的visibilityFilter:

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

    注意:

    1. 不要修改state

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

      Object.assign(state,{visibilityFilter:action.filter}),因爲它會改變第一個參數的值。你必須把第一個參數設置爲空對象。

    2. default情況下返回舊的state。遇到未知的action時,一定要返回舊的state

      Object.assign介紹

      Object.assign(target,source1,source2...sourceN)是ES6特性,用於對象的合併。

      //對象合併:source1,source2合併到target中。
      const target = { a: 1 };
      const source1 = { b: 2 };
      const source2 = { c: 3 };
      Object.assign(target, source1, source2);
      target // {a:1, b:2, c:3}
      
      //同名屬性的替換:source中a屬性值替換掉target中的a屬性值。
      const target = { a: { b: 'c', d: 'e' } }
      const source = { a: { b: 'hello' } }
      Object.assign(target, source)// { a: { b: 'hello' } }
      
      //數組的處理:assign會把數組視爲屬性名爲 0,1,2 的對象,
      //因此源數組的0號屬性值4覆蓋了目標數組的0號屬性值1。
      Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]
      
      

      注意:

      • 該方法的第一個參數是目標對象,後面的參數都是源對象;
      • 如果目標對象與源對象或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性;
      • Object.assign是淺拷貝。如果源對象某個屬性的值是對象,那麼目標對象拷貝得到的是這個對象的引用,這個對象的任何變化,都會反映到目標對象上面;
      • 同名屬性的替換。對於嵌套的對象,一旦遇到同名屬性,Object.assign的處理方法是替換,而不是添加。
  • 處理多個Action

    注意

    • 每個reducer只負責管理全局state中它負責的一部分。每個reducer的state參數都不同,分別對應它管理的那部分state數據。

    最後。Redux提供了combineReducers()工作類,用於生成一個函數,這個函數來調用你的一系列的reducer,每個reducer根據它們的key來篩選出state中的一部分數據並處理,然後這個生成的函數再將所有的reducer的結果合併成一個大的對象。

  • 代碼案例reducers.js

    import {combineReducers} from 'redux'
    //從actions文件中中導入action type常量和函數
    import {
      ADD_TODO,
      TOGGLE_TODO,
      SET_VISIBILITY_FILTER,
      VisibilityFilters
    } from './actions'
    
    //解構賦值
    const {SHOW_ALL} = VisibilityFilters
    
    //定義顯示篩選的reducer函數
    function visibilityFilter(state = SHOW_ALL,action){
      switch(action.type){
        case SET_VISIBILITY_FILTER:
          return action.filter
        default:
          return state   
      }
    }
    
    //定義處理事物的reducer函數
    function todos(state = [] , action){
      switch(action.type){
        case ADD_TODO:
          return [
            ...state,
            {
              text:action.text,
              completed:false 
            } 
          ]
        case TOGGLE_TODO:
          return state.map((todo,index)=>{
            if(index ==== action.index){
              return Object.assign({},todo,{
                completed:!todo.completed
              })
             }
            return todo
          })
        default:
          return state   
      }
    }
    
    //通過combineReducer函數將自定義的多個reducers關聯起來
    const todoApp = combineReducers(){
      visibilityFilter,
      todos
    }
    
    //導出該Reducer供外界使用
    export default todoApp;
    

Store

  • 簡介

    前面介紹了action來描述“發生了什麼”,使用reducers來根據action更新state的用法。

    Store 就是把action和reducer聯繫到一起的對象。Store有以下職責:

    • 維持應用的state;
    • 提供getState()方法獲取state;
    • 提供dispatch(action)方法更新state;
    • 通過subscribe(listener)註冊監聽器;
    • 通過subscribe(listener)返回的函數unsubscribe用於註銷監聽器。

    再次強調一下Redux應該只有一個單一的store。當要拆分數據處理邏輯時,你應該使用reducer組合而不是創建多個store。

  • createStore創建Store

    根據已有的reducer來創建store是非常容易的,前一節中我們通過combineReducers()將多個reducer合併爲一個。現我們將其導入,通過其使用createStore(reducers)創建Store:

    import {createStore} from 'redux'
    //導入定義好的reducers對象
    import todoApp from './reducers'
    //創建Store對象
    let store = createStore(todoApp)
    

    createStore()的第二個參數是可選的,用於設置state的初始狀態。這對開發同構應用時非常有用,服務器端redux應用的state結構可以於客戶端保持一致,那麼客戶端可以將從網絡接收到的服務端state直接用於本地數據初始化:

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

    經過上述幾個步驟,我們已經創建了actionreducerstore了,此時我們可以驗證一下,雖然沒有頁面,因爲它們都是純函數,只需要調用一下,對返回值做判斷即可。寫測試就這麼簡單。

    import {createStore} from 'redux'
    
    //1. 引入定義好的action
    import {
      addTodo,
      toggleTodo,
      setVisibilityFilter,
      VisibilityFilters} from './actions'
    
    //2. 引入定義好的reducer
    import todoApp from './reduces'
    
    //3. 創建store
    let store = createStore(todoApp)
    
    //4. 觸發action通過reducer更新state
    store.dispatch(addTodo('Learn about actions'))
    store.dispatch(addTodo('Learn about reducers'))
    store.dispatch(addTodo('Learn about store'))
    store.dispatch(toggleTodo(0))
    store.dispatch(toggleTodo(1))
    store.dispatch(setVisibilityFilter(VisibilityFilter.SHOW_COMPLETED))
    
    //5. 通過subscribe開啓監聽state更新,注意返回一個函數對象用於註銷監聽
    const unsubscribe = store.subscribe(()=>{
      console.log(store.getState())
    })
    
    //6. 停止監聽state更新
    unsubscribe();
    
    

State的基本結構

Redux 鼓勵你根據需要管理的數據來思考你的應用程序。數據就是你的應用state。

Redux state中頂層的狀態樹通常是一個普通的JavaScript對象(當然也可以是其他類型的數據,比如:數字、數據或者其他專門的數據結構,但大多數庫的頂層值都是一個普通對象)。

大多數應用會處理多種數據類型,通常可以分爲以下三類:

  • 域數據(Domain data):應用需要展示、使用或者修改的數據;
  • 應用狀態(App state):特定與應用某個行爲的數據;
  • UI狀態(UI state):控制UI如何展示的數據。

一個典型的應用state大致會長這樣:

{
  domainData1:{},   //數據域1
  domainData2:{},   //數據域2
  appState1:{},     //應用狀態域1
  appState2:{},     //應用狀態域1
  ui:{              //UI域
    uiState1:{},
    uiState2:{},
  } 
}

React-Redux 使用

這裏強調一下Redux和React之間沒有任何關係。Redux支持React、Angular、Ember、JQuery、甚至純JavaScript。

儘管此次,Redux還是和React和Deku這類庫搭配使用最好,因爲這類庫允許你以state函數的形式來描述界面,Redux通過action的形式來發起state變化。

安裝React Redux

Redux默認並不包含React綁定庫,需要單獨安裝。

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

核心API講解

1. Provider

provider組件是react-redux提供的核心組件,作用是將Redux Store提供可供內部組件訪問。

使用

通過react-redux提供的Provider組件包括其他組件

//導入Provider組件
import {Provider} from 'react-redux';
//導入創建好的Redux Store對象
import store from './store';
//導入組件
import CustomComponent from './CustomComponent';

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

connect組件也是react-redux提供的核心組件,作用是將當前組件與Redux Store進行關聯,以便通過mapStateToProps、mapDispatchToProp函數將當前組件Props與Redux Store 中的State和dispatch建立映射。

注意:

  1. 使用 connect() 前,需要先定義 mapStateToProps;
  2. 使用connect連接的組件需要被Provider組件包裹;
  3. mapStateToProps:這個函數來指定將當前組件Props與 Redux store state 建立映射關係。在每次 store 的 state 發生變化的時候,應用內所有組件的該函數都會被調用。如果不傳組件不會監聽Store State的變化,也就是說Store的更新不會引起UI的更新。
  4. mapDispatchToProps:這個函數用來指定將當前組件Props與store.dispatch建立映射關係。如果不傳React-Redux會自動將dispatch注入組件的props(可通過this.props.dispatch(action)使用)。

使用

在Provider組件包裹的組件通過react-redux提供的connect將組件與Redux Store進行連接。

//1. 引入 connect
import { connect } from 'react-redux';
class CustomComponent extends Component{
  render(){
    return (
       <View style={{flex: 1, flexDirection: 'column', alignItems: 'center'}}>
      		<Button title={'修改顯示數據'} 
                /**
                 *通過this.props.btnOnClick指定函數來間接觸發store.dispatch(action)
                 *更改Store中State。
                 */
                onPress={() => {this.props.btnOnClick();}}/>
          //通過屬性映射this.props.showText獲取到Redux Store State中的數據。
        	<Text style={{marginTop: 20}}>{this.props.showText}</Text>
      </View>
    );
  }
}

/**
 *2.創建mapStateToProp函數
 */
const mapStateToProps = (state) => {
    return {
        /**
         *將Redux Store State中的loginText映射到當前組件的showText屬性上,
         *然後當前組件通過this.props.showText即可獲取存儲在Store State中的loginText對應的值。
         */
        showText: state.loginText, 
        //將Redux Store State中的loginStatus狀態映射到當前組件的showStatus屬性上。
        showStatus: state.loginStatus,
    }
};

/**
 *3.創建mapDispatchToProp函數
 */
const mapDispatchToProps = (dispatch)=>{
  	return{
      /**
       *changeShowTex:爲自定義的Props屬性函數名,當前組件通過this.props.changeShowText()
       *即可觸發store.dispatch(action)來更新Redux Store State中數據。
       */
      changeShowText:()=>{
        dispatch(action);   //發送指定的action來更改Store中的State
      }
  	}
}

/**
 *4.通過connect函數將當前組件與Redux的Store連接起來。(當前組件需要被Provider組件包裹的)
 */
export default connect(mapStateToProps,mapDisPatchToProps)(CustomComponent);

完整示例代碼

實現目標:通過點擊頁面按鈕(觸發store.dispatch(action)),來更改當前顯示的文案信息(這裏的文案顯示信息存儲在Redux Store State中)

  1. 定義Action 類型

    //ActionType.js
    //登陸狀態
    const LOGIN_TYPE = {
        LOGIN_SUCCESS: 'LOGIN_SUCCESS',
        LOGIN_FAILED: 'LOGIN_FAILED',
        LOGIN_WAITING: 'LOGIN_WAITING',
    };
    export {LOGIN_TYPE};
    
  2. 創建Action(告訴Reducer要做什麼操作)

    //Actions.js
    //導入action types
    import {LOGIN_TYPE} from '../ActionType';
    
    export const loginWaiting = {
        type: LOGIN_TYPE.LOGIN_WAITING,
        text: '登陸中...',
    };
    
    export const loginSuccess = {
        type: LOGIN_TYPE.LOGIN_SUCCESS,
        text: '登陸成功...',
    };
    
    export const loginFailed = {
        type: LOGIN_TYPE.LOGIN_FAILED,
        text: '登陸失敗...',
    };
    
  3. 創建Reducer(根據傳遞進來的action type來處理相應邏輯返回新的state)

    //Reducer.js
    //導入action type
    import {LOGIN_TYPE} from '../ActionType';
    
    //默認的state
    const defaultState = {
        loginText: '內容顯示區',
        loginStatus: 0,
    };
    
    /**
     *創建reducer:
     *根據當前action類型更改Store State中的loginText和loginStatus,
     *會回調發送store.dispatch(action)事件組件的mapStateToProps函數。
     */
    const AppReducer = (state = defaultState, action) => {
        switch (action.type) {
            case LOGIN_TYPE.LOGIN_WAITING:    
                return {
                    loginText: action.text,
                    loginStatus: 0,
                };
            case LOGIN_TYPE.LOGIN_SUCCESS:
                return {
                    loginText: action.text,
                    loginStatus: 1,
                };
            case LOGIN_TYPE.LOGIN_FAILED:
                return {
                    loginText: action.text,
                    loginStatus: 2,
                };
            default:
                return state;
        }
    };
    export {AppReducer};
    
  4. 創建Redux Store

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

    //App.js
    //通過react-redux提供的Provider組件將store傳遞給子組件訪問
    import React,{Component} from 'react';
    import {Provider} from 'react-redux';
    import store from './store';
    import CustomComponent from '../page/CustomComponent';
    import CustomComponent2 from '../page/CustomComponent2';
    import CustomComponent3 from '../page/CustomComponent3';
    export default class App extends Component{
      render(){
        return(
          <Provider store={store}>
          	  <CustomComponent/>    //子組件
              <CustomComponent2/>   //子組件2
              <CustomComponent3/>   //子組件3
              ...
          </Provider>
        )
      } 
    }
    

    上面我們通過react-redux提供的Provider組件將我們創建好的store提供給子組件CustomComponent訪問,接下來我們看看子組件中如何與Redux Store建立關係,並訪問其State中內容。

  6. 子組件中訪問Redux Store State數據(以CustomComponent爲案例,其他子組件一樣)

    //CustomComponent.js
    import React,{Component} from 'react';
    import {View,Text,Button} from 'react-native';
    
    //導入Action,下面業務點擊要觸發dispatch(action)
    import * as Actions from '../actions/Actions';
    
    //1.導入connect,下面需要將當前組件與Redux Store通過該connect建立連接。
    import {connect} from 'react-redux';
    
    export default class CustomComponent extends Component{
      render(){
        return(
           <View style={{flex: 1, flexDirection: 'column', alignItems: 'center'}}>
                 /**
                  *5.當前組件通過this.props.xxx 指定mapStateToProps函數中自定義的屬性來獲取
                  *	 從Redux Store State映射的值。
                  */
                 <Text style={{marginTop: 20}}>{this.props.showText}</Text>
    
                 <Button title={'模擬登陸中'} onPress={() => {
                     /**
                      *6.當前組件通過this.props.xxx() 調用mapDispatchToProps自定義的函數,
                      *以此間接觸發store.dispatch(action)來發送action達到更新Store中State目的
                      */
                     //當connect第二個參數不傳遞的時候,Redux Store會自動將dispatch映射到Props上。
                     //this.props.dispatch(Actions.loginWaiting);} 
                     this.props.btnOnClick(1);}
                 }/>
                 <Button title={'模擬登陸成功'} onPress={() => {
                     this.props.btnOnClick(1);}
                 }/>
                 <Button title={'模擬登陸失敗'} onPress={() => {
                     this.props.btnOnClick(2);}
                 }/>
             </View>
        )
      }
    }
    
    /**
     *2.定義mapStateToProps函數(當Store中的State變化時候,會回調改函數)
     *	返回一個Object,內部是將state中的值映射到自定義的屬性上,以便當前組件通過this.props.xxx來
     *	獲取State中數據。
     */
    const mapStateToProps = (state)=>{
      	return{
            //將Redux Store State中的loginText映射到自定義的showText屬性上。
          	showText:state.loginText,
            //將Redux Store State中的loginStatus映射到自定義的showStatus屬性上。
          	showStatus:state.loginStatus,
        }
    }
        
    /**
     *3.定義mapDispatchToProps函數:
     *	返回一個Object,內部定義的屬性函數名稱,以便當前組件通過調用this.props.xxx()
     *	來間接觸發store.dispatch(action)。
     */
    const mapDispatchToProps = (dispatch)=>{
      return {
        changeShowText:(type)=>{
          switch(type){
            case 0:
              dispatch(Actions.loginWaiting);
              break;
            case 1:
              dispatch(Actions.loginSuccess);
              break;
            case 2:
               dispatch(Actions.loginFailed);
              break;   
           }
        }
      }
    }
    
    /**
     *4.通過connect將當前CustomComponent組件與Redux Store建立連接,並通過mapStateToProps、
     *	mapDispatchToProps函數將Redux Store State映射到當前組件Props中。
     */
    export default connect(mapStateToProps,mapDispatchToProps)(CustomComponent);
    

擴展:

1. 嵌套組件中訪問Redux Store State

如下組件:

根組件APP.js

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

子組件CustomComponent.js

//內部引入Child組件
return(
  ...
  <Child/>
)

我們在Child組件中如果要訪問Redux Store State與CustomComponent組件訪問方式一樣,如下:

Child.js

import React,{Component} from 'react';
import {View, Text, Button} from 'react-native';
//1.導入connect
import {connect} from 'react-redux';
import * as Actions from '../actions/CommonAction';
export default class Child extends Component{
  render(){
    return(
      <View>
        /**
         *使用:通過this.props.xxxx 指定mapStateToProps定義的屬性名
         *獲取Store State映射的數據。
         */
      	<Text>{this.props.xxxx}</Text>
        <Button onPress={()=>{
           /**
            *通過this.props.xxxx()調用mapStateToProps聲明的函數
            *間接觸發store.dispatch(action)來更新Redux Store State。
            */
           this.props.xxx();
        }}>
      </View>
    );
  }
  
//2.定義mapStateToProps函數
const mapStateToProps = (state)=>{
    return{
      //TODO...
    }
}

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

/**
 *4.通過connect將當前組件與Redux Store建立連接,並通過mapStateToProps、mapStateToProps函數
 *將Store 的 State和dispatch映射到Props中
 */
export default connect(mapStateToProps,mapStateToProps)(Child);

}
2. 使用combineReducers合併多個零散Reducer

上面的代碼中我們的Action以及Reducer都定義在一個文件中,對於中大型項目後期錯誤的排查和維護比較困難,因此我們重構項目,將Action和Reducer依據業務功能拆分使其各自獨立,通過藉助combineReducers對多個Reduce進行合併。

比如我們有登陸、註冊頁面,因此我們將原來的Action拆分成LoginAction、RegisterAction;將原來的Reducer拆分成LoginReducer、RegisterReducer使其各司其職處理相關的業務。

  1. 拆分Action

    LoginAction.js

    /**
     *登陸Action
     *PS:目前action觸發攜帶的是靜態數據,內部的data都是寫好的,
     *後面會擴展通過接口請求返回數據填充到data中
     */
    import {LOGIN_TYPE} from './ActionType';
    export const loginWaiting = {
        type: LOGIN_TYPE.LOGIN_WAITING,
        data: {
            status: 10,
            text: '登陸中...',
        },
    };
    export const loginSuccess = {
        type: LOGIN_TYPE.LOGIN_SUCCESS,
        data: {
            status: 11,
            text: '登陸成功!',
        },
    };
    export const loginFailed = {
        type: LOGIN_TYPE.LOGIN_FAILED,
        data: {
            status: 12,
            text: '登陸失敗!',
        },
    };
    

    RegisterAction.js

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

    LoginReducer.js

    //默認登陸頁面屬性
    const defaultLoginState = {
        Ui: {
            loginStatus: '',   //登陸狀態(用於控制登陸按鈕是否可點擊、以及顯示加載框等)
            loginText: '',     //登陸不同狀態下的提示的文字
        },
    };
    const LoginReducer = (state = defaultLoginState, action) => {
        switch (action.type) {
            case LOGIN_TYPE.LOGIN_WAITING:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            case LOGIN_TYPE.LOGIN_SUCCESS:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            case LOGIN_TYPE.LOGIN_FAILED:
                return {
                    ...state,
                    Ui: {
                        loginStatus: action.data.status,
                        loginText: action.data.text,
                    },
                };
            default:
                return state;
        }
    };
    export default LoginReducer;
    

    RegisterReducer.js

     //註冊頁面默認狀態
    const defaultRegisterState = {
        Ui: {
            registerStatus: '',   //登陸狀態(用於控制登陸按鈕是否可點擊、以及顯示加載框等)
            registerText: '',     //登陸不同狀態下的提示的文字
        },
    };
    const RegisterReducer = (state = defaultRegisterState, action) => {
        switch (action.type) {
            case REGISTER_TYPE.REGISTER_WAITING:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            case REGISTER_TYPE.REGISTER_SUCCESS:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            case REGISTER_TYPE.REGISTER_FAILED:
                return {
                    ...state,
                    Ui: {
                        registerStatus: action.data.status,
                        registerText: action.data.text,
                    },
                };
            default:
                return state;
        }
    };
    export default RegisterReducer;
    

    通過combineReducer({key1:reducer1,key2:reducer2})將reducer組合

    注意:

    • 使用combineReducers進行組合Reducer時候我們可指定Reducer名稱key,也可省略(默認使用Reducer導出的組件名)。
    • 通過combineReducers組合後,在展示組件中通過mapStateToProps函數映射時候我們需要指定combineReducers合併時指定的Reducer名稱來訪問Redux Store State中的數據(見下面案例)
    //合併Reducer
    import LoginReducer from './LoginReducer';
    import RegisterReducer from './RegisterReducer';
    const AppReducers = combineReducers({
        LoginReducer,                   //沒有指定LoginReducer名稱,Redux默認使用LoginReducer
        registerReducer:RegisterReducer,//指定LoginReducer名稱爲registerReducer,
    });
    export default AppReducers;
    

    後面的Reducer使用不變,通過指定AppReducers使用createStore來創建Store。

    重構以後運行看一下我們的State中的數據格式如下:

    /**
     *通過combineReducers組合後的Reducer,在Redux Store State中會
     *自動爲不同的Reducer添加名稱區分各自數據狀態區
     */
    {
    	"LoginReducer": {
    		"Ui": {
    			"loginStatus": 10,
    			"loginText": "登陸中..."
    		}
    	},
    	"registerReducer": {
    		"Ui": {
    			"registerStatus": "",
    			"registerText": ""
    		}
    	}
    }
    

    接下來我們在展示組件的mapStateToProps中將Redux Store State映射到展示組件的Props中

    登陸頁面/組件(Login.js)

    //定義connect函數第一個參數:將Store中的state映射到當前組件的props上
    const mapStateToProps = (state) => {return {
            /**
             *這裏我們通過state.xxx方式將問Redux Store State中屬性映射到當前組件Props屬性上。
             *其中[xxx] 爲combineReducers合併Reducer時指定的名稱(沒指定默認使用組件導出名)
             */
            loginShowText: state.LoginReducer.Ui.loginText,
            loginShowStatus: state.LoginReducer.Ui.loginStatus,
        };
    };
    

    註冊頁面/組件(Register.js)

    //定義connect函數第一個參數:將Store中的state映射到當前組件的props上
    const mapStateToProps = (state) => {
        return {
            /**
             *通過state.xxxx 指定 combineReducers 合併Reducer時指定的名稱,
             *將Redux Store State屬性映射到展示組件的屬性上。
             */
            registerShowText: state.registerReducer.Ui.registerText,
            registerShowStatus: state.registerReducer.Ui.registerStatus,
        };
    };
    

    重構後的項目結構如下,這樣我們就可以根據業務模塊進行鍼對性的處理Action和Reducer內業務邏輯,使其邏輯更清晰,提高項目的可讀性和維護性。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-u7Lga7FK-1579244723039)(/Users/zhangchao/Desktop/學習歸檔/RN學習筆記/Redux狀態容器/images/rn重構.png)]

3. 使用bindActionCreators簡化Action的分發

什麼是bindActionCreators?

bindActionCreators作用是在使用redux的connect將react與redux store關聯起來的connectmapDispatchToProps函數中將單個或多個Action Creator轉化爲dispatch(action)的函數集合形式。開發者不用再手動dispatch(actionCreator(type)),而是可以直接調用方法。

bindActionCreators原理

bindActionCreators實際上就是將dispatch直接和單個或多個action creator結合好然後發出去的這一部分操作給封裝成一個函數。bindActionCreators 會使用dispatch將這個函數發送出去。

使用與不使用bindActionCreators對比

假如我們通過action creator來創建action

UserAction.js

//添加用戶的同步action
export const addUser = (user) => {
    return {
        type: ADD_USER,
        user,
    };
};
//刪除用戶的同步action
export const removeUser = (user)=>{
    return {
        type: REMOVE_USER,
        user,
    };
}
//計算總用戶數量
export const sumUser = () => {
    return {
        type: SUM_USER,
    };
};

然後在TestComponent.js組件中通過mapDispatchToProps函數中使用:

  • 不使用bindActionCreators

    //1.導入UserAction
    import {addUser,removeUser,sumUser} from './actions/UserAction';
    
    //2.定義connect的mapDispatchToProps函數
    const mapDispatchToProps = (dispatch)=>{
      return{
        propsAddUser:(user)=>{
          //通過dispatch來分發指定的Action
          dispatch(addUser(user))
        },
        propsRemoveUser:(user)=>{
          dispatch(removeUser(user))
        },
        propsSumUser:()=>{
          dispatch(sumUser())
        }
      }
    }
    //通過connect將react與Redux Store關聯並導出組件
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //3.組件內調用mapDispatchToProps中定義的映射的props
    <Button title='添加用戶' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中國上海'
         }
         //通過this.props.xxx 指定調用mapDispatchToProps中定義的屬性即可。
         this.props.propsAaddUser(user);
         //this.props.propsRemoveUser(user);
         //this.props.propsSumUser();
    }}>
    
  • 使用bindActoinCreators

    格式:

    bindActionCreators(actionCreators,dispatch)

    參數:

    • actionCreators:(函數對象):也可以是一個對象,這個對象的所有元素都是action create函數。
    • dispatch:(功能):在Store實例dispatch上可用的功能。

    示例:

    //1.導入UserAction
    import {addUser,removeUser,sumUser} from './actions/UserAction';
    
    //2.導入redux中的bindActionCreators
    import {bindActionCreators} from 'redux';
    
    //3.定義connect的mapDispatchToProps函數
    const mapDispatchToProps = (dispatch)=>{
      return{
        /**
         *使用bindActionCreators將多個action creator轉換成dispatch(action)形式,
         *此處不在手動調用disptch(action)了。
         */
         actions:bindActionCreators({
           propsAddUser:(user)=>addUser(user),
           propsRemoveUser:(user)=>removeUser(user),
           propsSumUser:sumUser,  //不帶參數的action creator
         },dispatch),
      }
    }
    //通過connect將react與Redux Store關聯並導出組件
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //4.組件內調用mapDispatchToProps中定義的映射的props
    <Button title='添加用戶' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中國上海'
         }
         /**
          *通過this.props.actions.xxxx 指定調用mapDispatchToProps中
          *bindActionCreators定義的props即可。
          */
         this.props.actions.propsAddUser(user);
         //this.props.actions.propsRemoveUser(user);
         //this.props.actions.propsSumUser();
    }}>
    

    (推薦)通過import * as xxx的形式將一個文件中的所有action creator全部導入方式實現

    //1.導入UserAction中所有的action creator
    import * as UserActions from './actions/UserAction';
    
    //2.導入redux中的bindActionCreators
    import {bindActionCreators} from 'redux';
    
    //3.定義connect的mapDispatchToProps函數
    const mapDispatchToProps = (dispatch)=>{
      return {
        /**
         *通過bindActionCreators將UserAction.js中所有的action creator轉換成dispatch(action)
         *形式,此處不在手動調用disptch(action)了。
         */
        actions:bindActionCreators(UserActions,dispatch);
      }
    }
    //通過connect將react與Redux Store關聯
    export default connect(mapStateToProps,mapDispatchToProps)(TestComponent)
    
    //4.組件內調用mapDispatchToProps中定義的映射的props
    <Button title='添加用戶' onpress={()=>{
         let user={
           name:'zcmain',
           age:20,
           address:'中國上海'
         }
         //通過this.props.actions.xxxx 指定調用UserAction.js中具體的action即可。
         this.props.actions.addUser(user);
         //this.props.actions.removeUser(user);
         //this.props.actions.sumUser();
    }}>
    

對於異步Action如何使用bindActionCreator

在我們使用redux-thunk時候通過創建返回函數方式實現異步Action,那麼在bindActionCreators中如何使用異步Action呢?其實與同步Action使用沒有太大區別。如果異步Action調用的函數有返回值,並且通過bindActionCreator綁定次異步函數後,我們在通過this.props.xxxx (xxx爲函數名)調用異步函數時候直接接受返回值。

異步Action(UserAction.js)

//通過創建返回函數方式創建Action
const addUser = (user)=>{
  return (dispatch)=>{
     //異步Action有返回值
     return await new Promise((resolve, reject) => {
            //模擬異步網絡請求
            setTimeout(() => {
                dispatch(changeLoginBtnEnable(true));
                let userInfo = {
                        userId: 10001,
                        realName: 'zcmain',
                        address: '中國上海',
                    };
                 dispatch(updateUserInfoVo(userInfo));
                 resolve('success');
                }, 2000);
            });
       }
 }

bingActionCreators進行綁定異步Action

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

const mapDispatchToProps = (dispatch)=>{
  return{
    //通過bindActionCreatros將action creator轉換成dispatch(action)
    actions:bindActionCreatros(UserActions,dispatch);
  }
}

組件中使用異步Action

...
<Button title='添加用戶',onPress={()=>{
    let user={
      id:'1',
      name:'zcmain',
    }
    /**
     *通過bindActionCreator綁定後的action creator的調用函數如果有返回值,
     *通過this.props.屬性調用時候直接接受返回
     */
    this.props.actions.addUser(user)
      .then((response)=>{
           //TOOD...
      		 console.log('成功:' + JSON.stringify(response);
    	},(error)=>{
      		 console.log('失敗:' + error.message);
    	}).catch((exception)=>{
      		 console.log('異常:' + JSON.stringify(exception));
    });
}}/>

bindActionCreators源碼解析

  1. 判斷傳入的參數是否是object,如果是函數,就直接返回一個包裹dispatch的函數;
  2. 如果是object,就根據相應的key,生成包裹dispatch的函數即可;
/**
 *bindActionCreators函數
 *參數說明:
 *@param actionCreators: action create函數,可以是一個單函數,也可以是一個對象,這個對象的所有元素
 *都是action create函數;
 *@param dispatch: store.dispatch方法;
 */
export default function bindActionCreators(actionCreators, dispatch) {
  /**
   *如果actionCreators是一個函數的話,就調用bindActionCreator方法對action create函數
   *和dispatch進行綁定。
   */
  if (typeof actionCreators === 'function') {
      return bindActionCreator(actionCreators, dispatch)
  }
  /**
   *如果actionCreators不是一個對象或者actionCreators爲空,則報錯
   */
  if (typeof actionCreators !== 'object' || actionCreators === null) {
      throw new Error('bindActionCreators expected an object or a function, + 
                      'instead received ${actionCreators === null ?+
                      'null' : typeof actionCreators}.' +
                      'Did you write "import ActionCreators from" instead of +
                      '"import * as ActionCreators from"?')
  }

  //否則actionCreators是一個對象獲取所有action create函數的名字
  const keys = Object.keys(actionCreators)
  //遍歷actionCreators數組對象保存dispatch和action create函數進行綁定之後的集合
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    // 排除值不是函數的action create
    if (typeof actionCreator === 'function') {
      // 進行綁定
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  //返回綁定之後的對象
 return boundActionCreators
}

/**
 *bindActionCreator函數
 */
function bindActionCreator(actionCreator, dispatch) {
  // 這個函數的主要作用就是返回一個函數,當我們調用返回的這個函數的時候,就會自動的dispatch對應的action
  // 這一塊其實可以更改成如下這種形式更好
  // return function(...args) {return dispatch(actionCreator.apply(this, args))}
  return function() { return dispatch(actionCreator.apply(this, arguments)) }
}

高級

異步Action

前面我們將的action創建都是同步狀態,當dispatch(action)時候,state會被立即更新。

創建同步Action(返回的是一個action對象):

//創建同步action
export const syncAddItem = {
  type:'addItem',
  text:'增加一條數據',
}
//或者通過函數創建(可接收參數,返回action)
export const syncAddItem=(desc)=>{
  return{
    type:'addItem',
    text:desc,
  }
}

//通過store觸發同步action,State會立即被更新
store.dispatch(syncAddItem);
store.dispatch(syncAddItem('增加一條數據'));

對於異步action創建我們需要藉助**Redux Thunk**中間件。 action創建函數除了返回action對象外還可以返回函數。這時這個action創建函數就成爲了thunk

什麼是(爲什麼使用)Redux Thunk?

我們之所以需要使用諸如 Redux-Thunk 之類的中間件,是因爲 Redux 存儲僅支持同步數據流。 於是,中間件來救援了! 中間件允許異步數據流,解釋您分派的任何內容,並最終返回一個允許同步 Redux 數據流繼續的普通對象。 因此,Redux 中間件可以解決許多關鍵的異步需求(例如 axios 請求)。

Redux Thunk 中間件允許您編寫返回函數替代返回action對象。可以使用thunk中間件來進行延遲動作的分派,或者僅在滿足某個條件時才分發。內部函數接收store的dispatchgetState作爲參數。

當action創建函數返回函數時,這個函數會被Redux Thunk middleWare執行(如下創建的異步Action返回的return (dispatch)函數會被Thunk中間件執行),這個函數並不需要保持純淨;它可以帶有副作用,包括執行異步API請求。這個函數還可以執行dispatch(action),就像dispatch同步的Action一樣。

Redux Thunk在異步Action中使用

1. 創建異步Action(返回是一個函數會被Thunk中間件調用):

//創建一個異步的action,
export const asyncAction1 = (str) => {
    //返回一個接收dispatch參數的函數(該函數會被Thunk中間件調用),
    return (dispatch) => {
        //2秒後指定其他操作,比如觸發dispatch(action)更新State
        setTimeout(() => {
            //dispatch(action);
            console.log(str);
        }, 2000);
    };
};

//storet通過dispatch方法分發異步Action
store.dispatch(asyncAction1('異步Action創建函數'))

當然異步Action返回函數除了接收dispatch參數外還可以接受getState參數,我們可以根據getState中的狀態來進行邏輯判斷執行不同的dispatch:

//創建一個異步的action,
export const asyncAction1 = (str) => {
    //返回一個接收dispatch和getState參數的函數(該函數會被Thunk中間件調用),
    return (dispatch,getState) => {
      //通過getState獲取State中的counter屬性,如果爲偶數則返回不觸發dispatch
       const {counter} = getState();
       if(counter % 2 === 0){
          return;
       }
       //否則2秒後指定其他操作,比如觸發dispatch(action)更新State
       setTimeout(() => {
            //dispatch(action);
            console.log(str);
        }, 2000);
    };
};

//store調用dispatch方法
store.dispatch(asyncAction1('異步Action創建函數接收dispatch和getState屬性'))

異步Action返回函數除了可以接收dispatchgetState兩個參數以外,還可以通過Redux Thunk 使用withExtraArgument 函數注入自定義參數:

//通過Redux Thunk的withExtraArgument注入自定義參數到異步Action返回函數中
import {createStore,applyMiddleWare} from 'redux';
import thunk from 'redux-thunk';

//單個參數注入
const name ='zcmain';

//多個參數包裝成對象注入
const age = 20,
const city = 'ShanHai',
const userInfo = {
  name,
  age
  city,
}

const store = createStore(
  reducer,
  //創建Store時候將自定的參數通過thunk.withExtraArgument注入到異步Action返回函數中
  applyMiddleware(thunk.withExtraArgument(name,userInfo)),
);
//異步Action
const asyncAction1 = ()=>{
  /**
   *返回函數接受三個參數,其中 name、userInfo 是通過
   *Thunk.withExtraArgument(name,userInfo)注入的自定義參數。
   */
  return (dispatch,getState,name,userInfo)=>{
       //TODO...you can use name and userInfo here
  }
}

Thunk middleware 中間件調用的函數可以有返回值,它會被當作 dispatch 方法的返回值傳遞。

//創建一個異步的Action
export const asyncAction2 = (url) => {
    //返回一個接收dispatch參數的函數
    return (dispatch) => {
        /**
         *thunk middleWare調用的函數返回一個Promise對象,它會被當作 dispatch 方法的返回值傳遞,
         *這裏通過Fetch網絡請求,響應結果後調用dispatch(action)來更新State。
         *注意:
         *  不要使用 catch,因爲會捕獲,在 dispatch 和渲染中出現的任何錯誤,
         *  導致 'Unexpected batch number' 錯誤。
         *  https://github.com/facebook/react/issues/6895
         */
        return fetch(url)
          .then((response) =>{
                 response.json()
               },(error)=>{
          
           })
          .then((json) => {
            //收到相應後發送dispatch(action)來更新State
            dispatch({type:'UPDATA',data:json});
            return json;
        });
    };
};

/**
 *store通過dispatch方法觸發異步Action,因爲該action返回函數有返回值,
 *會被當作是dispatch方法的返回值傳遞。
 */
store.dispatch(asyncAction2('http://xxx.xxx.xxx.xxx:xxx/test/json))
     .then((json)=>{
      //TODO...
     }).catch(()=>{
      //TODO...
});

注意:

Thunk middleWare執行有返回值的函數中不要使用 catch,因爲會捕獲,在 dispatch 和渲染中出現的任何錯誤, Unexpected batch number 錯誤

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

2. 使用異步Action

上面我們創建好了異步的Action,接下來我們需要通過Redux Thunk中間件來使用該異步Action

  1. 安裝redux-thunk庫:

    npm -i --save redux-thunk
    #或者
    yarn add redux-thunk
    
  2. 通過Redux的applyMiddleWare使用Redux Thunk來創建store:

    AppStore.js

    //引入createStore、applyMiddleWare
    import {createStore,applyMiddleWare} from 'redux';
    //引入thunk中間件
    import thunk from 'redux-thunk';
    //引入reducers
    import reducers from '../reducers/AppReducers';
    
    //指定reducer和中間件來創建store
    const store = createStore(reducers,applyMiddleWare(thunk));
    
    //導出store
    export default store;
    
  3. 其他組件使用

    LoginComponent.js

    /**
     *登陸頁面的mapDispatchToProps函數中使用dispatch方法觸發異步的action,會在兩秒後打印日誌;
     *注意:mapDispatchToProps函數要依賴react-redux的Provider和connect。
     */
    const mapDispatchToProps = (dispatch) => {
        return {
            onclick: () => {
                dispatch(LoginAction.asyncAction1('發送異步action'));
            },
        };
    };
    
    //兩秒後會打印
    LOG  str:發送異步Actoin啦...
    
    /**
     *dispatch分發有有返回值的異步asyncAction2
     */
    const mapDispatchToProps = (dispatch) => {
        return {
            onclick: () => {
                dispatch(LoginAction.asyncAction2('發送異步action接受返回值'))
                 .then((json)=>{
                     console.log('dispatch 分發異步Action並接收返回值:' + json)
                 });
            },
        };
    };
    

範式化數據

爲什麼要設置範式化state數據結構?

事實上,大部分程序處理的數據都是嵌套或互相關聯的,那麼如何在state中使用嵌套及重複的數據(對象複用)?例如:

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

上面的數據結構比較複雜,並且有部分數據是重複的。這裏還存在一些讓人關心的問題:

  • 難以保證所有複用的數據同時更新:當數據在多處冗餘後,需要更新時,很難保證所有的數據都進行更新。
  • 嵌套複雜度高:嵌套的數據意味着 reducer 邏輯嵌套更多、複雜度更高。尤其是在打算更新深層嵌套數據時。
  • 不可變的數據在更新時需要狀態樹的祖先數據進行復制和更新,並且新的對象引用會導致與之 connect 的所有 UI 組件都重複 render。儘管要顯示的數據沒有發生任何改變,對深層嵌套的數據對象進行更新也會強制完全無關的 UI 組件重複 render。

正因爲如此,在 Redux Store 中管理關係數據或嵌套數據的推薦做法是將這一部分視爲數據庫,並且將數據按範式化存儲。

設計範式化State數據結構

範式化結構包含以下幾個方面:

  1. 任何類型的數據在state中都有自己的"表";
  2. 任何 “數據表” 應將各個項目存儲在對象中,其中每個項目的 ID 作爲 key,項目本身作爲 value
  3. 任何對單個項目的引用都應該根據存儲項目的 ID來完成。
  4. ID 數組應該用於排序。

上面博客示例中的 state 結構範式化之後可能如下:

authorcomments對象提取出來通過byId來使用

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

表間關係

因爲我們將Redux Store視爲數據庫,所以在很多數據庫設計規則裏面也是同樣適用的。例如:對於多對多的關係,可以設計一張中間表用於存儲相關聯的項目ID(經常被稱爲相關表或者關聯表)。爲了一致性起見,我們還會使用相同的byIdallIds用於實際的數據項表中。

entities:{
  authors:{byId:{},allIds:[]},
  book:{byId:{},allIds:[]},
  authorBook:{
    byId:{
      1:{
        id : 1,
        authorId : 5,
        bookId : 22
      },
      2:{
        id : 2,
        authorId : 5,
        bookId : 15,
      }
    },
    allIds:[1,2]
  }  
}

嵌套數據範式化

因爲 API 經常以嵌套的形式發送返回數據,所以該數據需要在引入狀態樹之前轉化爲規範化形態。Normalizr 庫可以幫助你實現這個。你可以定義 schema 的類型和關係,將 schema 和響應數據提供給 Normalizr,他會輸出響應數據的範式化變換。輸出可以放在 action 中,用於 store 的更新。有關其用法的更多詳細信息,請參閱 Normalizr 文檔。

管理範式化數據

當數據存在ID、嵌套或者關聯關係時,應當以範式化形式存儲:對象只能存儲一次,ID作爲鍵值,對象間通過ID相互引用。

將Store類比於數據庫,每一項都是獨立的"表"。normalizrredux-orm此類的庫能在管理規範化數據時提供參考和抽象。

中間件使用

redux-thunk

Redux Thunk是Redux中提供異步Action處理的中間件,具體使用參考上面文章《什麼是Redux Thunk

redux-saga

redux-saga也是用於解決RN中異步交互的問題,與redux-thunk目標一致,不同點在於:

  • redux-thunk:

    • 介紹:是redux推出一個MiddleWare,使用簡單,允許action 創建函數除了返回 action 對象外還可以返回函數,並且該返回函數可以接受dispatchgetState作爲參數。這個函數並不需要保持純淨;它還可以帶有副作用,包括執行異步 API 請求。這個函數還可以 dispatch action。
    • 優點:代碼量小,上手簡單適合輕小型應用程序中。
    • 缺點:返回函數內部複雜,不易維護。由於thunk使得Action創建函數返回不再是一個action對象,而是一個函數,而函數的內部可以多種多樣,甚至更爲複雜,顯然使得action不易於維護。
  • redux-saga

    • 介紹:官網上的描述redux-saga 是一個用於管理應用程序 Side Effect(副作用,例如異步獲取數據,訪問瀏覽器緩存等)的 library,它的目標是讓副作用管理更容易,執行更高效,測試更簡單
    • 優點:避免回調地獄(當前thunk 使用async/await也可以解決),方便測試和維護,適合大型應用程序。
    • 缺點:陡峭學習路線,樣板代碼量大。

    在許多正常情況下和中小型應用程序中,使用async / await風格redux-thunk。它可以爲你節省很多樣板代碼/操作/類型,而且你不需要在很多不同的sagas.ts之間切換,也不需要維護-一個特定的sagas樹。但是,如果你正在開發一個大型的應用程序,其中包含非常複雜的異步,並且需要一些特性,比如併發/並行模式,或者對測試和維護有很高的需求(尤其是在測試驅動開發中),那麼redux -sagas可能會拯救你的生命。

    參見《Redux-Thunk vs. Redux-Saga

redux-ignore

redux-ignore可以指定reducer函數觸發條件(例如:指定某個/某些actions纔會觸發當前reducer函數)。

對於通過combineReducers合併拆分的Reducer來說,觸發每個 action 都會調用 所有的 reducer,JavaScript 引擎有足夠的能力在每秒運行大量的函數調用,而且大部分的子 reducer 只是使用 switch 語句,並且針對大部分 action 返回的都是默認的 state。如果你仍然關心 reducer 的性能,可以使用類似 redux-ignore工具,確保只有某些action會調用一個 reducer 或幾個reducer。

  • 安裝redux-ignore

    npm -i --save redux-ignore
    #或者
    yarn add redux-ignore
    
  • 配置combineReducer組合Reducer,並通過filterActions(也可通過ignoreActions忽略指定的action)指定能夠觸發Reducer執行的action

    import {combineReducers} from 'redux';
    import {filterActions} from 'redux-ignore/src';
    import {reducerA} from './ReducerA';
    import {reducerB} from './ReducerB';
    
    //通過combineReducers將分散的Reducer組合成一個reducers
    const reducers = combineReducers(
        reducerA:fileterAction(
      			    reducerA,
                    /**
                     *指定能夠觸發reducerA執行的Action數組,只有dispatch該數組內的action,
                     *reducerA纔會調用。
                     */
      			     [
      					'actionA1',       
      					'actionA2'
      					 ...
      				 ]),
      
        reducerB:fileterAction(
          		    reducerB,
          		    /**
                     *指定能夠觸發reducerB執行的Action數組,只有dispatch該數組內的action,
                     *reducerA纔會調用。
                     */
          			[
                        'actionB1',
                        'actionB2'
                         ...
                    ]);
    )
    

reselect

什麼是reselect?

reselect可以作爲 redux 的一箇中間件,它通過傳入的多個state計算獲得新的 state,然後傳遞到 Redux Store。其主要就是進行了中間的那一步計算,使得計算的狀態被緩存,從而根據傳入的 state 判斷是否需要調用計算函數(selectTodos),而不用在組件每次更新的時候都進行調用,從而更加高效。

在Redux中使用用於優化mapStateToProps中需要大量計算的業務邏輯

安裝Reselect

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

我們來看一個connect中的mapStateToProps函數

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

/**
 *mapStateToProps 就是一個 selector,每次State變化時候就會被調用。
 *【缺點】每次組件更新的時候都會執行selectTodos函數重新計算visibleTodos,如果計算量比較大,
 * 會造成性能問題。
 */
const mapStateToProps = (state) => {
    return{
        //調用selectTodos函數根據條件計算todos
        visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    }
};

之前 connect 函數實現的時候,我們知道映射 props 的函數被 store.subscribe(),因此每次組件更新的時候,無論 state 是否改變,都會調用 mapStateToProps,而 mapStateToProps 在計算 state 的時候就會調用 state 計算函數selectTodos,過程 如下:

store.subscribe()(註冊事件) —>狀態更新時調用 mapStateToProps(一個selector,返回 state) —> 調用 state 計算函數 selectTodos

那麼,問題來了,如果 selector 的計算量比較大,每次更新的重新計算就會造成性能問題。

而解決性能問題的 出發點 就是:避免不必要的計算

解決問題的方式:從 selector 着手,即 mapStateToProps,如果 selector 接受的狀態參數不變,那麼就不調用計算函數,直接利用之前的結果。

Reselect 提供 createSelector 函數來創建可記憶的 selector。

createSelector函數原型:

createSelector(…inputSelectors|[inputSelectors],resultFunc)

該函數接受一個或者多個selectors,或者一個selectors數組,計算他們的值並且作爲參數傳遞給resultFunc,

createSelector通過判斷input-selector之前調用和之後調用的返回值是否全等於來覺得是否調用resultFunc

  • 第一個參數:多個inputSelector或一個inputSelector數組
  • 第二個參數:轉換函數

注意:

  1. 如果 state tree 的改變會引起 input-selector 值變化,那麼 selector 會調用轉換函數,傳入 input-selectors 作爲參數,並返回結果。
  2. 如果 input-selectors 的值和前一次的一樣,它將會直接返回前一次計算的數據,而不會再調用一次轉換函數。

因此我們可以將之前的state 計算函數selectTodos放在createSelector函數的第二個參數轉換函數內部,只有state改變引起input-selector值變化纔會調用轉換函數重新進行state計算,否則直接使用之前的結果,避免了再一次進行state值計算。

使用Reselect

我們通過使用reselect對上面的代碼state計算進行優化

//導入createSelector函數
import {createSelector} from 'reselect';

//定義getTodos input-selector(接收參數state)
const getTodos = (state)=>{
  return state.todos;
};

//定義getVisibilityFilter input-selector(接收參數state)
const getVisibilityFilter = (state){
  return state.visibilityFilter
};

/**
 *一個 state 計算函數(假如內部有很大的計算量)。
 *使用createSelector函數優化:根據input-selector創建記憶的Selector。
 *將計算邏輯放在轉換函數中。
 */
const selectTodos =createSelector(
      //第一個參數是input-selector數組
  		[getTodos,getVisibilityFilter],
      //第二個桉樹爲轉換函數,僅當state變更引起input-selector改變纔會觸發(內部執行大量計算)
      (todos,visibilityFilter)=>{
        switch (visibilityFilter) {
          case 'SHOW_ALL':
            return todos
          case 'SHOW_COMPLETED':
            return todos.filter(todo => todo.completed) //過濾todos數組中已完成的todos
          case 'SHOW_ACTIVE':
            return todos.filter(todo => !todo.completed)//過濾todos數組中未完成的todos
        }
      }
);

我們使用react-redux可以在mapStateToProps()中當作正常函數來調用可記憶的selector

/**
 *mapStateToProps 中調用記憶的selector函數selectTodos
 *當state的變化引起input-selector值改變的時候纔會觸發createSelector中轉換函數的執行進行計算,
 *否則跳過計算直接使用緩存數據。
 */
const mapStateToProps = (state) => {
   return{
      //調用可記憶的selector(參數依據input-selector接收的參數類型來定)
      visibleTodos: selectTodos(state),   
   }
};

在上例中, getTodosgetVisibilityFilter 都是 input-selector。因爲他們並不轉換數據,所以被創建成普通的非記憶的 selector 函數。

但是,selectTodos 是一個可記憶的 selector。他接收getTodosgetVisibilityFilter 爲 input-selector,還有一個轉換函數來計算過濾的 todos 列表。這樣當state的變化引起input-selector值改變的時候纔會觸發createSelector中轉換函數的執行進行計算,否則跳過計算直接使用緩存數據,避免不必要的計算。

組合Selector

上面我們指定input-selector和轉換函數通過createSelector創建了可記憶的Selector(selectTodos)。同時可記憶的Selector自身也可作爲其它可記憶的selector的input-selector。我們把上面可記憶的selectTodos 當作另一個可記憶selector的input-selector,來進一步通過關鍵字(keyword)過濾todos

//創建input-selector
const keyword = (state)=>{
  return state.keyword;
}
//創建組合Selector
const getVisibleTodosFilterByKeyword = createSelector(
      /**
       *第一個參數input-selector數組:
       *selectTodos:上面創建可記憶的selector。
       *keyword :本次創建的input-selector。
       */
  		[selectTodos,keyword],
      /**
       *第二個參數:轉換函數,
       *參數位:input-selector數組中返回值
       */
      (visibleTodos,keyword)=>{
        return visibleTodos.filter((todo)=>{
          //指定關鍵字通過對可記憶的selectTodos篩選的結果進一步篩選後返回。
          todo.text.indexOf(keyword)>1
        });
      }
);

/**
 *mapStateToProps 中調用記憶的組合selector函數getVisibleTodosFilterByKeyword
 */
const mapStateToProps = (state) => {
    return{
        visibleTodos: getVisibleTodosFilterByKeyword(state),
    }
};

在Selectors中訪問React props

到目前爲止,我們只看到selector接收Redux store state作爲參數,然而,selector也可以接收props。

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

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

//創建可記憶的selector,同上代碼一樣不變,省略...
const selectTodos = createSelector(
  		[getTodos,getVisibilityFilter],
      (todos,visibleFilter)=>{
         //TODO...
      }
);

mapStateToProps()中調用將props傳遞給可記憶的selector selectTodos()函數

const mapStateToProps = (state,props)=>{
   return{
      //調用可記憶的selector將state和props傳遞過去
      visibleTodos: selectTodos(state,props),
   }
}

注意:

使用createSelector創建的selector只有在參數集與之前的參數集相同時纔會返回緩存值。

例如一個組件A中多次使用同一個組件B,但是多個B組件中的屬性props不一樣,就會導致組件B中的selector無效,因爲同一個組件但是參數集props不一樣了,會導致B組件selector重新計算)。

//組件A
export default A extends Component{
  render(){
    return(
      <View>
       /**
        *多次使用組件B,但是每個B組件中index屬性值都不同,這會導致B組件中的selector無效。
        *每次執行B組件都輸導致selector重新計算。
        */
       <B index=1/>
       <B index=2/>
       <B index=3/>
      </View>
    );
  }
}

同個組件多個實例的共享Selector

上面我們說了createSelector創建的selector只有在參數集與之前參數集相同纔會返回緩存值,那麼我們想要在一個組件多個實例中共享selector該如何實現呢?

例如我們在A組件中使用B組件的多個實例,如何讓這些B組件實例共享selector呢?

解決方案:

組件的各個實例需要他們自己的selector備份。

實現方案:

  1. 創建一個函數,這個函數每次調用的時候返回一個新的selector

    /**
     *1.定義input-selector
     */
    const getTodos = (state)=>{
      return state.todos;
    }
    
    const getVisibleFilter = (state)=>{
      return state.visibleFilter;
    }
    
    /**
     *2.創建可記憶的selector
     *舊方案:
     *		此時的selector直接可被mapStateToProps調用了,缺點是同一個組件不同的實例
     *		如果屬性(參數值)不同,都會觸發selector重新計算。
     *優化:
     *		該selector不直接讓mapStateToProps調用,而是通過一個函數返回。
     */
    const selectTodos = createSelector(
    	  [getTodos,getVisibleFilter],
          (todos,visibilityFilter)=>{
            switch (filter) {
              case 'SHOW_ALL':
                return todos
              case 'SHOW_COMPLETED':
                return todos.filter(todo => todo.completed) //過濾todos數組中已完成的todos
              case 'SHOW_ACTIVE':
                return todos.filter(todo => !todo.completed)//過濾todos數組中未完成的todos
            }
        }
    );
    
    /**
     *3.創建一個函數,返回可記憶的selector
     */
    const makeSelectTodos = ()=>{
      return selectTodos();
    }
    
  2. 給組件實例設置各自獲取私有的selector的方法。mapStateToProps函數可以實現。

    知識點

    • 如果connect函數的mapStateToProps返回的不是一個對象而是是一個函數,他就可以被用來爲每個組件的容器創建一個私有的mapStateProps函數。如下:
    /**
     *舊的方案:
     *mapStateToProps函數直接返回了對象,並且對象內部直接調用可記憶的selector函數,
     *缺點是不同實例的屬性不同,會導致selector失效,每次調用都會導致selector重新計算。
     */
    const mapStateToProps = ()=>{
      return{
         //直接調用可記憶的selectTodos函數
          visibleTodos: selectTodos(state,props),
      }
    }
    
    
    /**
     *優化:
     *創建一個函數,內部調用上一步中創建的返回可記憶的selector的函數來獲取各自實例私有的selector。
     *然後該函數返回一個mapStateToProps函數(mapStateToProps函數中調用私有的selector)
     */
    const makeMapStateToProps =()=>{
      //獲取實例私有的selector
      const getSelectTodos = makeSelectTodos();
      //創建mapStateToProps函數,內部調用私有的selector
      const mapStateToProps = (state,props)=>{
        return{
          //調用實例私有的selector函數
           visibleTodos: getSelectTodos(state,props),
        }
      }
      //返回這個mapStateToProps函數
      return mapStateToProps;
    }
    
  3. 最後將這個makeMapStateToProps傳遞到connect中,那麼組件容器的每一個實例中將會獲得各自含有私有的selector的mapStateToProps的函數。

    export default connect(makeMapStateToProps,mapDispatchToProps)(XXXX);
    

redux-router

redux-promise

RN項目目錄結構

目前的項目比較簡單結構RN部分如下:
外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8ZuOFbBu-1579244723042)(/Users/zhangchao/Desktop/學習歸檔/RN學習筆記/Redux狀態容器/images/目錄結構.png)]

  • common:主要存放一些公用的模塊。比如對話框、導航欄、樣式表、自定義的小組件等。
    在這裏插入圖片描述

  • constant:存放常量。比如應用常量和配置信息。
    在這裏插入圖片描述

  • page:存放UI相關的組件和實現
    在這裏插入圖片描述
    注意:項目中使用了react-redux和一些MiddleWare,因此將UI實現相關的邏輯統一放在業務模塊下以便於快速定位和排查問題。例如login模塊下:

    • LoginAction:業務模塊所需的Action

      負責創建業務所需的Action和Action Creator(異步Action業務邏輯處理)。

    • LoginComponent:業務的展示組件

      僅負責UI的渲染。

    • LoginContainer:業務容器組件

      負責將展示組件與Redux Store通過**connect**函數關聯,並將Store state和dispatch映射到展示組件Props中。可通過在mapDispatchToProps()函數中使用bindActionCreators()函數來簡化dispatch(action)分發。

    • LoginReducer:業務模塊reducer函數。

      負責根據Action類型更新Store State。

  • redux:存放redux涉及的actionType、經過combineReducers函數合併的reducer、Redux Store:
    在這裏插入圖片描述

  • utils:存放一些工具類。比如數據持久化存儲、字符編碼、哈希散列、加解密、網絡請求等工具類。
    在這裏插入圖片描述

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章