首頁 > webfront > ECMAS > react > > 正文

再談redux實現原理分析與優化工程設計—redux

發布人:[email protected]    點擊:

redux有三大準則單一數據源:整個應用狀態,都應該被存儲在單一store的對象樹中。只讀狀態:唯一可以修改狀態的方式,就是發送(dispatch)

Redux 的設計思想很簡單

  1. Web 應用是一個狀態機,視圖與狀態是一一對應的。

  2. 所有的狀態,保存在一個對象里面。

在組件化的應用中(比如react、vue2.0等),會有著大量的組件層級關系,深嵌套的組件與淺層父組件進行數據交互,變得十分繁瑣困難。而redux,站在一個服務級別的角度,可以毫無阻礙地(這個得益于react的context機制,后面會講解)將應用的狀態傳遞到每一個層級的組件中。redux就相當于整個應用的管家。

redux有三大準則

  1. 單一數據源:整個應用狀態,都應該被存儲在單一store的對象樹中。

  2. 只讀狀態:唯一可以修改狀態的方式,就是發送(dispatch)一個動作(Action),通俗來講,就是說只有getter,沒有setter。

  3. 使用純函數去修改狀態:純函數保障了狀態的穩定性,不會因不同環境導致應用程序出現不同情況,聽說是redux真正的精髓,日后可以深入了解。

redux的幾個概念

  1. Action:唯一可以改變狀態的途徑,服務器的各種推送、用戶自己做的一些操作,最終都會轉換成一個個的Action,而且這些Action會按順序執行,這種簡單化的方法用起來非常的方便。Action 是一個對象:const action = {type: 'UPDATE_DEMO',a: 'Write Document'}; 

  2. Reducer:當dispatch之后,getState的狀態發生了改變,Reducer就是用來修改狀態的。Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State。

    import {List, Map} from 'immutable';
    let stateInit = Map({
        a:''
    });
    export default function demoReducer(state = stateInit, action = {}) {
        switch (action.type) {
            case "UPDATE_DEMO":
                state = state.set("a", action.msg);
                return state;
            default :
                return state
        }
    }
  3. Store:管理著整個應用的狀態,Store提供了一個方法dispatch,這個就是用來發送一個動作,去修改Store里面的狀態,然后可以通過getState方法來重新獲得最新的狀態,也就是state。

    注冊store tree

    Redux通過全局唯一的store對象管理項目中的state:

    store = createStore(reducer,initialState);

    可以通過store注冊listener,注冊的listener會在store tree每次變更后執行

    store.subscribe(function () {
      console.log("state change");
    });

    更新store tree

    store調用dispatch,通過action把變更的信息傳遞給reducer

    store根據action攜帶type在reducer中查詢變更具體要執行的方法,執行后返回新的state

    export function updateDemoDataAciton(selectBankCard){
       return (dispatch) => {
          dispatch({
             type:"UPDATE_DEMO",
             Object
          })
       }
    }

在 Redux 的源碼目錄 src/,我們可以看到如下文件結構:

├── utils/

│     ├── warning.js # 打醬油的,負責在控制臺顯示警告信息

├── applyMiddleware.js

├── bindActionCreators.js

├── combineReducers.js

├── compose.js

├── createStore.js

├── index.js # 入口文件

下面結合代碼,分析redux的實現。

Store實現

Store — 數據存儲中心,同時連接著Actions和Views(React Components)

連接的意思大概就是:

  • Store需要負責接收Views傳來的Action

  • 然后,根據Action.type和Action.payload對Store里的數據進行修改

  • 最后,Store還需要通知Views,數據有改變,Views便去獲取最新的Store數據,通過setState進行重新渲染組件(re-render)。

Store的主要方法:

  • createStore createStore方法用來注冊一個store,返回值為包含了若干方法的對象

  • combineReducers 存在的目的就是解決了整個store tree中state與reducer一對一設置的問題

  • bindActionCreators

  • bindActionCreators

  • applyMiddleWare

  • compose


createStore源碼分析

createStore(
    reducer:(state, action)=>nextState, //reducer必須是一個function類型,此方法根據action.type更新state
    preloadedState:any, //store tree初始值
    enhancer:(store)=>nextStore//enhancer通過添加middleware,增強store功能【很牛逼,可以實現中間件、時間旅行,持久化等】
)=>{
    getState:()=>any,//讀取store tree中所有state
    subscribe:(listener:()=>any)=>any,//注冊listener,監聽state變化。Redux采用了觀察者模式,store內部維護listener數組,用于存儲所有通過store.subscrib注冊的listener,store.subscrib返回unsubscrib方法,用于注銷當前listener;當store tree更新后,依次執行數組中的listener。【可以理解成是 DOM 中的 addEventListener】
    dispatch:(action:{type:""})=>{type:""},//分發action 1、根據action查詢reducer中變更state的方法,更新store tree,2、變更store tree后,依次執行listener中所有響應函數
    replaceReducer:(nextReducer:(state, action)=>nextState)=>void//替換reducer,改變state更新邏輯【一般在 Webpack Code-Splitting 按需加載的時候用】

}
Redux 規定:
  • 一個應用只應有一個單一的 store,其管理著唯一的應用狀態 state

  • 不能直接修改應用的狀態 state

  • 若要改變 state,必須 dispatch 一個 action,這是修改應用狀態的唯一途徑

combineReducers源碼分析

若整個項目只通過一個reducer方法維護整個store tree,隨著項目功能和復雜度的增加,我們需要維護的store tree層級也會越來越深,當我們需要變更一個處于store tree底層的state,reducer中的變更邏輯會十分復雜且臃腫。

而combineReducers存在的目的就是解決了整個store tree中state與reducer一對一設置的問題。我們可以根據項目的需要,定義多個子reducer方法,每個子reducer僅維護整個store tree中的一部分state, 通過combineReducers將子reducer合并為一層。這樣我們就可以根據實際需要,將整個store tree拆分成更細小的部分,分開維護。


Screen Shot 2019-01-30 at 7.40.25 PM.jpg

store enhancer基本概念及使用

這部分,其實事件項目,也鮮有關心,推薦閱讀《淺析Redux 的 store enhancer


redux應用總結:

  • store 由 Redux 的 createStore(reducer) 生成

  • state 通過 store.getState() 獲取,本質上一般是一個存儲著整個應用狀態的對象

  • action 本質上是一個包含 type 屬性的普通對象,由 Action Creator (函數) 產生

  • 改變 state 必須 dispatch 一個 action

  • reducer 本質上是根據 action.type 來更新 state 并返回 nextState 的函數

  • reducer 必須返回值,否則 nextState 即為 undefined

  • 實際上,state 就是所有 reducer 返回值的匯總(本教程只有一個 reducer,主要是應用場景比較簡單)

  • Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

關于redux教程,建議通讀:《Redux 進階教程

immutable(數據不可變)的作用

React在利用組件(Component)構建Web應用時,其實無形中創建了兩棵樹:虛擬dom樹和組件樹,就像下圖所描述的那樣(原圖):


react-redux原理分析

react技術棧不像angular,其進階路線如下:

React ——> React + redux + React-redux ——> React + redux + React-redux + React-router

React其實跟Redux沒有直接聯系,也就是說,Redux中dispatch觸發store tree中state變化,并不會導致React重新渲染。react-redux才是真正觸發React重新渲染的模塊。

redux與react-redux關系圖

react-redux是一個輕量級的封裝庫,核心方法只有兩個:

  • Provider

  • connect

實際應用如下:

const rootReducer = combineReducers({...});
function configureStore(preloadedState) {
    const store = createStore(
        rootReducer,
        preloadedState,
        enhancer
    )
}
const store = configStore();
render((            ), document.getElementById('view'));

下面我們來逐個分析其作用

Provider模塊的功能

主要分為以下兩點:

  • 封裝原應用:在原應用組件上包裹一層,使原來整個應用成為Provider的子組件

  • 傳遞store:接收Redux的store作為props,通過context對象傳遞給子孫組件上的connect

connect模塊的功能

connect模塊才是真正連接了React和Redux

function mapStateToProps(state) {
    return {
        loading: state.global.get('loading'),
        demoData: state.demoData.get('demoData'),
    }
}
function mapDispatchToProps(dispatch) {
    return bindActionCreators(updateDemoDataAciton, dispatch);
}
class DemoComponent extends React.Component {}
const DemoContainer = connect(mapStateToProps, mapDispatchToProps)(DemoComponent);

connect完整函數聲明如下:

connect=(
    mapStateToProps(state,ownProps)=>stateProps:Object, 
    mapDispatchToProps(dispatch, ownProps)=>dispatchProps:Object, 
    mergeProps(stateProps, dispatchProps, ownProps)=>props:Object,
    options:Object
)=>(
    WrappedComponent
)=>component

再來看下connect函數體結構,我們摘取核心步驟進行描述

export default function connect (mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
    //... 參數處理
    return function wrapWithConnect (WrappedComponent) {
        class Connect extends Component {
            constructor (props, context) {
                super(props, context);
                // 從祖先Component處獲得store
                this.store = props.store || context.store;
                this.stateProps = computeStateProps(this.store, props);
                this.dispatchProps = computeDispatchProps(this.store, props);
                this.state = {storeState: null};
                // 對stateProps、dispatchProps、parentProps進行合并
                this.updateState();
            }
            shouldComponentUpdate (nextProps, nextState) {
                // 進行判斷,當數據發生改變時,Component重新渲染
                if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
                    this.updateState(nextProps);
                    return true;
                }
            }
            componentDidMount () {
                // 改變Component的state
                this.store.subscribe(() => {
                    this.setState({
                        storeState: this.store.getState()
                    });
                });
            }
            //... 周期方法及操作方法
            render () {
                this.renderedElement = createElement(WrappedComponent, his.mergedProps); //mearge stateProps, dispatchProps, props);
                return this.renderedElement;
            }
        }
        return hoistStatics(Connect, WrappedComponent);
    };
}

connect模塊返回一個wrapWithConnect函數,wrapWithConnect函數中又返回了一個Connect組件。

Connect組件的功能有以下兩點:

  1. 包裝原組件,將state和action通過props的方式傳入到原組件內部

  2. 監聽store tree變化,使其包裝的原組件可以響應state變化

此部分內容來自《Redux原理(一):Store實現分析》,推薦閱讀原文。

再次也建議讀一下《React.js 的 context》React.js 的 context 就是這么一個東西,某個組件只要往自己的 context 里面放了某些狀態,這個組件之下的所有子組件都直接訪問這個狀態而不需要通過中間組件的傳遞。一個組件的 context 只有它的子組件能夠訪問,它的父組件是不能訪問到的,你可以理解每個組件的 context 就是瀑布的源頭,只能往下流不能往上飛。

Redux如何設計action、reducer、selector

錯誤1:以API為設計State的依據

以API為設計State的依據,往往是一個API對應一個子State,State的結構同API返回的數據結構保持一致(或接近一致)。

但是API是基于服務端邏輯設計的,而不是基于應用的狀態設計的。

錯誤2:以頁面UI為設計State的依據

頁面UI需要什么樣的數據和數據格式,State就設計成什么樣。這種方式的優點就是模塊與模塊之間互相獨立,不會相互影響,每個頁面維護自己的reducer,并且只在store中存入該頁面展示或者變化的最小數據,使用起來很方便,不太需要關心其他模塊的緩存數據,比較符合redux設計的初衷(并不是用來做一個前端數據庫)

以頁面UI為設計State存在的問題:一、這種State依然存在數據重復的問題,二、當新增或修改一條記錄時,需要修改不止一個地方。


合理設計State

最重要最核心的原則是像設計數據庫一樣設計State

把State看做一個數據庫,State中的每一部分狀態看做數據庫中的一張表,狀態中的每一個字段對應表的一個字段。設計一個數據庫,應該遵循以下三個原則:

  1. 數據按照領域(Domain)分類,存儲在不同的表中,不同的表中存儲的列數據不能重復。

  2. 表中每一列的數據都依賴于這張表的主鍵。

  3. 表中除了主鍵以外的其他列,互相之間不能有直接依賴關系。

這三個原則,可以翻譯出設計State時的原則:

  1. 把整個應用的狀態按照領域(Domain)分成若干子State,子State之間不能保存重復的數據。

  2. State以鍵值對的結構存儲數據,以記錄的key/ID作為記錄的索引,記錄中的其他字段都依賴于索引。

  3. State中不能保存可以通過已有數據計算而來的數據,即State中的字段不互相依賴。

具體推薦閱讀《如何優雅的設計Redux的Store中的State樹》、《Redux進階系列3:如何設計action、reducer、selector


用localStorage緩存Redux的state

基于Redux+React構建的單頁面應用組件的 大部分狀態 (一些非受控組件內部維護的state,確實比較難去記錄了)都記錄在Redux的store維護的state中。

正是因為Redux這種基于全局的狀態管理,才讓“UI模型”可以清晰浮現出來。

所以,只要在瀏覽器的本地存儲(localStorage)中,將state進行緩存。

一種簡(愚)單(蠢)的方式是,在每次state發生更新的時候,都去持久化一下。這樣就能讓本地存儲的state時刻保持最新狀態。

基于Redux,這也很容易做到。在創建了store后,調用subscribe方法可以去監聽state的變化。

// createStore之后
store.subscribe(() => {
  const state = store.getState();
  saveState(state);
})

顯然,從性能角度這很不合理(不過也許在某些場景下有這個必要)。所以機智的既望同學,提議只在onbeforeunload事件(刷新或關閉)上就可以。

window.onbeforeunload = (e) => {
  const state = store.getState();
 saveState(state,version);//讀取state的時候,則要比較代碼的版本和state的版本,不匹配則進行相應處理。版本維護方便
};


只需要在應用初始化的時候,Redux創建store的時候取一次就可以,就可以(基本)還原用戶最后的交互界面了。




參考文章:

redux深入理解

Redux原理(一):Store實現分析

react-redux原理分析

用localStorage緩存Redux的state

關于Redux框架,Reducer中state處理方式的探討

Redux 簡明教程redux中間件的原理——從懵逼到恍然大悟

Redux 卍解

Redux 思想和源碼解讀(二)

解讀redux工作原理