Redux PK Mobx,誰更適合管理大規模應用的前端狀態?

作者:龔麒,廣發證券信息技術部高級前端工程師,負責廣發證券金鑰匙項目的React Native改造,及廣發證券有問必答業務的微信小程序版的開發,在混合開發和各類跨端解決方案應用上具有豐富經驗。
責編:陳秋歌([email protected]
2017 年 7 月 8 日(星期六),「“CSDN前端開發”在線峯會」將在 CSDN 學院召開,龔麒已受邀參加,並圍繞前端狀態管理,深度分享廣發證券在該領域做出的一系列探索與實踐。屆時,您將瞭解當前最爲火熱的兩款前端狀態管理工具,學會評析不同狀態管理方案的,並在選型時做出合理取捨。

前言

前端的發展日新月異,Angular、React、Vue 等前端框架的興起,爲我們的應用開發帶來新的體驗。React Native、Weex、微信小程序等技術方案的出現,又進一步擴展了前端技術的應用範圍。隨着這些技術的革新,我們可以更加便利地編寫更高複雜度、更大規模的應用,而在這一過程中,如何優雅的管理應用中的數據狀態成爲了一個需要解決的問題。

爲解決這一問題,我們在項目中相繼嘗試了當前熱門的前端狀態管理工具,對前端應用狀態管理方案進行了深入探索與思考。

Dive into Redux

Redux 是什麼

Redux 是前端應用的狀態容器,提供可預測的狀態管理,其基本定義可以用下列公式表示:

(state, action) => newState

其特點可以用以下三個原則來描述。

  • 單一數據源

    在 Redux 中,整個應用的狀態以狀態樹的形式,被存儲在一個單例的 store 中。

  • 狀態數據只讀

    惟一改變狀態數據的方法是觸發 action,action 是一個用於描述已發生事件的普通對象。

  • 使用純函數修改狀態

    在 Redux 中,通過純函數,即 reducer 來定義如何修改 state。

從上述原則中,可以看出,構成 Redux 的主要元素有 action、reducer、store,借用一張經典圖示(見圖1),可以進一步理解 Redux 主要元素和數據流向。

圖片描述

圖1 展示了Redux的主要元素和數據流向

探索 The Redux way

異步方案選型

Redux 中通過觸發 action 修改狀態,而 action 是一個普通對象,action creator 是一個純函數。如何在 action creator 中融入、管理我們的異步請求,是在實際開發中首先需要解決的問題。

當前 Redux 生態活躍,出現了不少異步管理中間件。在我們的實踐中,認爲大致可以分爲兩類。

(1)以 redux-thunk 爲代表的中間件

使用 redux-thunk 完成一個異步請求的過程如下:

//action creator
function loadData(userId){
    return (dispatch,getState) => {
        dispatch({type:'LOAD_START'})
        asyncRequest(userId).then(resp=>{
            dispatch({type:'LOAD_SUCCESS',resp})
        }).catch(error=>{
            dispatch({type:'LOAD_FAIL',error})
        })
    }
}

//component
componentDidMount(){
    this.props.dispatch(loadData(this.props.userId));
}

在上述示例中,引入 redux-thunk 後,我們將異步處理和業務邏輯定義在一個方法中,利用中間件機制,將方法的執行交由中間件管理。

上例是一個簡單的異步請求,在代碼中我們需要主動地根據異步請求的執行狀態,分別觸發請求開始、成功和失敗三個 action。這一過程顯得繁瑣,當應用中有大量這類簡單請求時,項目中會充滿這種重複代碼。

針對這一問題,出現了一些用於簡化這類簡單請求的工具。實際開發中,我們選擇了 redux-promise-middleware 中間件,使用這一中間件來完成上述請求的代碼示例如下:

//action creator
function loadData(userId){
    return {
        type:types.LOAD_DATA,
        payload:asyncRequest(userId)
    }
}

//component
componentDidMount(){
    this.props.dispatch(loadData(this.props.userId));
}

引入 redux-promise-middleware 中間件,我們在 action creator 中返回一個與 redux action 結構一致的普通對象,不同的是,payload 屬性是一個返回 Promise 對象的異步方法。通過將異步方法的執行過程交由 redux-promise-middleware 中間件處理,中間件會幫助我們處理異步請求的狀態,根據異步請求的結果爲當前操作類型添加 PEDNGING/FULFILLED/REJECTED 狀態,我們的代碼得到大幅簡化。

redux-promise-middleware 中間件適用於簡化簡單請求的代碼,開發中推薦混合使用 redux-promise-middleware 中間件和 redux-thunk。

(2)以 redux-saga 爲代表的中間件

以 redux-thunk 爲代表的中間件可以滿足一般的業務場景,但當業務對用戶事件、異步請求有更細粒度的控制需求時,redux-thunk 不能便利的滿足。此時,可以選擇以 redux-saga 爲代表的中間件。

redux-saga 可以理解爲一個和系統交互的常駐進程,其中,Saga 可簡單定義如下:

Saga = Worker + Watcher

採用 redux-saga 完成異步請求,示例如下:

//saga
function* loadUserOnClick(){
    yield* takeLatest('LOAD_DATA',fetchUser); 
} 

function* fetchUser(action){
    try{
        yield put({type:'LOAD_START'});
        const user = yield call(asyncRequest,action.payload);
        yield put({type:'LOAD_SUCCESS',user});
    }catch(err){
        yield put({type:'LOAD_FAIL',error})
    }
}

//component
<div onclick={e=>dispatch({type:'LOAD_DATA',payload:'001'})}>load data</div>

與 redux-thunk 相比,使用 redux-saga 有幾處明顯的變化:

  • 在組件中,不再 dispatch(action creator),而是 dispatch(pure action)
  • 組件中不再關注由誰來處理當前 action,action 經由 root saga 分發;
  • 具體業務處理方法中,通過提供的 call/put 等幫助方法,聲明式的進行方法調用;
  • 使用 ES6 Generator 語法,簡化異步代碼語法。

除去上述這些不同點,redux-saga 真正的威力,在於其提供了一系列幫助方法,使得對於各類事件可以進行更細粒度的控制,從而完成更加複雜的操作

簡單列舉如下:

  • 提供 takeLatest/takeEvery/throttle 方法,可以便利的實現對事件的僅關注最近事件、關注每一次、事件限頻;
  • 提供 cancel/delay 方法,可以便利的取消、延遲異步請求;
  • 提供 race(effects),[…effects] 方法來支持競態和並行場景;
  • 提供 channel 機制支持外部事件。

在 Redux 生態中,除了 redux-saga 中間件,還有另一箇中間件,redux-observable 也可以滿足這一場景。

redux-observable 是基於 RxJS 的用於處理異步請求的中間件,藉助 RxJS 的各種操作符和幫助方法,redux-observable 也能實現對各類事件的細粒度操作,比如取消、限頻、延遲請求等。

redux-saga 與 redux-observable 適用於對事件操作有細粒度需求的場景,同時他們也提供了更好的可測試性,當你的應用逐漸複雜需要更加強大的工具時,他們會成爲很好的幫手。

應用狀態設計

如何設計應用狀態的數據結構是一個值得思考的問題,在實踐中,我們總結了兩點數據劃分的指導性原則,應用狀態扁平化和抽離公共狀態。

(1) 應用狀態扁平化

在我們的項目中,有聯繫人、聊天消息和當前聯繫人對象。最初我們採用如下數據結構:

{
contacts:[
    {
        id:'001',
        name:'zhangsan',
        messages:[
            {
                id:1,
                content:{
                    text:'hello'
                },
                status:'succ'
            },
            ...
        ]
    },
   ...
],
selectedContact:{
        id:'001',
        name:'zhangsan',
        messages:[
            {
                id:1,
                content:{
                    text:'hello'
                },
                status:'succ'
            },
            ...
        ]
    }
}

採用上述數據機構,帶來幾個問題。

  • 消息對象與聯繫人對象耦合,消息對象的變更操作引發聯繫人對象的變更操作;
  • 聯繫人集合和當前聯繫人對象數據冗餘,當數據更新時需要多處修改來保持數據一致性;
  • 數據結構嵌套過深,不便於數據更新,一定程度上導致更新時的耗時增加。

將數據扁平化、解除耦合,得到如下數據結構:

{
contacts:[
    {
        id:'001',
        name:'zhangsan'
    },
    ...
],
messages:{
    '001':[
        {
           id:1,
           content:{
               text:'hello'
           },
           status:'succ'
       },
       ...
    ],
    ...
},
selectedContactId:'001'
}

相對於之前的問題,上述數據結構具有以下優點:

  • 細粒度的更新數據,進而細粒度控制視圖的渲染;
  • 結構清晰,避免更新數據時,複雜的數據操作;
  • 去除冗餘數據,避免數據不一致。
(2)抽離公共狀態

在領域對象之外,往往還有另外一些與請求過程相關的狀態數據,如下所示:

{
  user: {
    isError: false, // 加載用戶信息失敗
    isLoading: false, // 加載用戶中
    ...
    entity: { ... },
  },
  messages: {
    isLoading: true, // 加載消息中
    nextHref: '/api/messages?offset=200&size=100', // 消息分頁數據
    ...
    entities: { ... },
  },
  authors: {
    isError: false, // 加載作者失敗
    isLoading: false, // 加載作者中
    nextHref: '/api/authors?offset=50&size=25', // 作者分頁數據
    ...
    entities: { ... },
  },
}

上述數據結構中,我們按照功能模塊將狀態數據內聚。採用上述結構,會導致我們需要寫很多基本重複的 action,如下所示:

{
  type: 'USER_IS_LOADING',
  payload: {
    isLoading,
  },
}

{
  type: 'MESSAGES_IS_LOADING',
  payload: {
    isLoading,
  },
}

{
  type: 'AUTHORS_IS_LOADING',
  payload: {
    isLoading,
  },
}
...

我們分別爲 user 、message 、author 定義了一系列 action,它們作用類似,代碼重複。爲解決這一問題,我們可以將這類狀態數據抽離,不再簡單的按照功能模塊內聚,抽離後的狀態數據如下所示:

{
  isLoading: {
    user: false,
    messages: true,
    authors: false,
    ...
  },
  isError: {
    userEdit: false,
    authorsFetch: false,
    ...
  },
  nextHref: {
    messages: '/api/messages?offset=200&size=100',
    authors: '/api/authors?offset=50&size=25',
    ...
  },
  user: {
    ...
    entity: { ... },
  },
  messages: {
    ...
    entities: { ... },
  },
  authors: {
    ...
    entities: { ... },
  },
}

採用這一結構,可以避免定義大量相似的 action type,避免編寫重複的 action。

修改狀態數據

將應用狀態數據不可變化是使用 Redux 的一般範式,有多種方式可以實現不可變數據的效果,我們分別嘗試了 immutable.js 和 seamless-immutable.js,並在實際開發中選擇了 seamless-immutable.js。

(1)immutable.js

immutable.js 是一個知名度很高的不可變數據實現庫。它爲人稱道的是基於共享數據結構所帶來的數據修改時的高性能,但是在我們的使用過程中,發現其易用性不夠友好,使用體驗並不美好。

  • 首先,immutable.js 實現的是 shallowly immutable,如下示例中,notFullyImmutable 中的對象屬性仍然是可變的。
var obj = {foo: "original"};
var notFullyImmutable = Immutable.List.of(obj);

notFullyImmutable.get(0) // { foo: 'original' }

obj.foo = "mutated!";

notFullyImmutable.get(0) // { foo: 'mutated!' }
  • 另外,immutable.js 使用了自定義的數據結構,這意味着貫穿我們的應用都需要明確當前使用的是 immutable.js 的數據結構。獲取數據時,需要使用 get 方法,而不能使用 obj.prop 或者 obj[prop]。在需要將數據同外部交互,如存儲或者請求時,需要將特有數據結構轉換成原生 JavasScript 對象。

  • 最後,以 state.set('key',obj) 形式更新狀態時,obj 對象不能自動的 immutable 化。

(2)seamless-immutable.js

上述問題使得我們在開發中不斷地需要停下來思考當前寫法是否正確,於是我們繼續嘗試,最後選擇使用 seamless-immutable.js 來幫助實現不可變數據。

seamless-immutable.js 意爲無縫的 immutable,與 immutable.js 不同,它沒有定義新的數據結構,其基本使用如下所示:

var array = Immutable(["totally", "immutable", {hammer: "Can’t Touch This"}]);

array[1] = "I'm going to mutate you!"
array[1] // "immutable"

array[2].hammer = "hm, surely I can mutate this nested object..."
array[2].hammer // "Can’t Touch This"

for (var index in array) { console.log(array[index]); }
// "totally"
// "immutable"
// { hammer: 'Can’t Touch This' }

根據我們的使用體驗,seamless-immutable.js 易用性優於 immutable.js。但是在選擇之前,有一點需要了解的是,在數據修改時,seamless-immutable.js 性能低於 immutable.js。數據嵌套層級越深,數據量越大,性能差異越明顯。這裏需要根據業務特點來做選擇,我們的業務沒有大批量的深度數據修改需求,易用性比性能更重要。

在應用中使用

Redux 可以應用在多種場景,在我們的開發中,已經將它應用到了 React Native、Angular 1.x 重構和微信小程序的項目上。

在前文介紹 Redux 三原則時提到,Redux 具有單一數據源,觸發 action 時,Redux store 在執行狀態更新邏輯後,會執行註冊在 store 上的事件處理函數。

基於上述過程,在簡單的 HTML 中可以如下使用 Redux:

const initialState = {count:0};
const counterReducer = (state=initialState,action) => {...}

const {createStore} = Redux;
const store = createStore(counterReducer);

const renderApp = () =>{
    const {count} = store.getState();
    document.body.innerHTML = `
    <div>
        <h1>Clicked : ${count} times</h1>
        <button onclick="()=>{store.dispatch({type:'INCREMENT'})}">
            INCREMENT
        </button>
    </div>
    `;
};

store.subscribe(renderApp);
renderApp();

結合前端框架使用 Redux 時,社區中已經有了 react-redux、ng-redux 這類的幫助工具,甚至對應微信小程序,也有了類似的實現方案。其實現原理均一致,都是通過全局對象綁定 Redux store,使得在應用組件中可以獲得 store 中的狀態數據,並向 store 註冊事件處理函數,用來在狀態變更時觸發視圖的更新。

當我們在項目中應用 Redux 時,也對代碼文件的組織進行了一番探索。通常我們按照如下方式組織代碼文件:

|--components/
|--constants/
 ----userTypes.js
|--reducers/
 ----userReducer.js
|--actions/
 ----userAction.js

嚴格遵循這一模式並無不可,但是當項目規模逐漸擴大,文件數量增多後,切換多文件夾尋找文件變得有些繁瑣,在這一時刻,可以考慮嘗試 Redux Ducks 模式。

|--components
 |--redux
  ----userRedux

所謂 Ducks 模式,也即經典的鴨子類型。這裏將同一領域內,Redux 相關元素的文件合併至同一個文件 userRedux 中,可以避免爲實現一個簡單功能頻繁在不同目錄切換文件。

與此同時,根據我們的使用經驗,鴨子模式與傳統模式應當靈活的混合使用。當業務邏輯複雜,action與reducer各自代碼量較多時,按照傳統模式拆分可能是更好的選擇。此時可以如下混合使用兩種模式:

|--modules/
 ----users/
 ------userComponent.js
 ------userConstant.js
 ------userAction.js
 ------userReducer.js
 ----messages/
 ------messageComponent.js
 ------messageRedux.js

Dive into Mobx

Mobx 是什麼

Mobx 是一個簡單、可擴展的前端應用狀態管理工具。Mobx 背後的哲學很簡單:當應用狀態更新時,所有依賴於這些應用狀態的觀察者(包括UI、服務端數據同步函數等),都應該自動得到細粒度地更新。

Mobx 中主要包含如下元素:

  • State

    State 是被觀察的應用狀態,狀態是驅動你的應用的數據。

  • Derivations

    Derivations 可以理解爲衍生,它是應用狀態的觀察者。Mobx 中有兩種形式的衍生,分別是 Computed values 和 Reactions。其中,Computed values 是計算屬性,它的數據通過純函數由應用狀態計算得來,當依賴的應用狀態變更時,Mobx 自動觸發計算屬性的更新。Reactions 可簡單理解爲響應,與計算屬性類似,它響應所依賴的應用狀態的變更,不同的是,它不產生新的數據,而是輸出相應的副作用(side effects),比如更新UI。

  • Actions

    Actions 可以理解爲動作,由應用中的各類事件觸發。Actions 是變更應用狀態的地方,可以幫助你更加清晰的組織你的代碼。

Mobx 項目主頁中的示例圖(見圖2),清晰的描述了上述元素的關係:

圖片描述

Mobox項目主頁的示例圖

探索 The Mobx Way

在探索 Redux 的過程中,我們關注異步方案選型、應用狀態設計、如何修改狀態以及怎樣在應用中使用。當我們走進 Mobx,探索 Mobx 的應用之道時,分別從應用狀態設計、變更應用狀態、響應狀態變更以及如何在實際、複雜項目中應用進行了思考。

應用狀態設計

設計應用狀態是開始使用 Mobx 的第一步,讓我們開始設計應用狀態:

class Contact {
    id = uuid();
  @observable firstName = "han";
  @observable lastName = "meimei";
  @observable messages = [];
  @observable profile = observable.map({})
  @computed get fullName() {
        return `${this.firstName}, ${this.lastName}`;
    }
}

上述示例中,我們定義領域模型 Contact 類,同時使用 ES.next decorator 語法,用 @observable 修飾符定義被觀察的屬性。

領域模型組成了應用的狀態,定義領域模型的可觀察屬性是 Mobx 中應用狀態設計的關鍵步驟。在這一過程中,我們需要關注兩方面內容,分別是 Mobx 數據類型和描述屬性可觀察性的操作符。

(1)Mobx 數據類型

Mobx 內置了幾種數據類型,包括 objects、arrays、maps 和 box values。

  • objects 是 Mobx 中最常見的對象,示例中 Contact 類的實例對象,或者通過 mobx.observable({ key:value}) 定義的對象均爲 Observable Objects。
  • box values 相對來說使用較少,它可以將 JavaScript 中的基本類型如字符串轉爲可觀察對象。
  • arrays 和maps 是 Mobx 對 JavaScript 原生 Array 和 Map 的封裝,用於實現對更復雜數據結構的監聽。

當使用 Mobx arrays 結構時,有一個需要注意的地方,如下所示,經封裝後,它不再是一個原生 Array 類型了。

Array.isArray(observable([1,2,3])) === false

這是一個我們最初使用時,容易走進的陷阱。當需要將一個 observable array 與第三方庫交互使用時,可以對它創建一份淺複製,像下面這樣,轉爲原生 JavaScript:

Array.isArray(observable([]).slice()) === true

默認情況下,領域模型中被預定義爲可觀察的屬性才能被監聽,而爲實例對象新增的屬性,不能被自動觀察。使用 Mobx maps,即使新增的屬性也是可觀察的,我們不僅可以響應集合中某一元素的變更,也能響應新增、刪除元素這些操作。

除了使用上述幾種數據類型來定義可觀察的屬性,還有一個很常用的概念,計算屬性。通常,計算屬性不是領域模型中的真實屬性,而是依賴其他屬性計算得來。系統收集它對其他屬性的依賴關係,僅當依賴屬性變更時,計算屬性的重新計算纔會被觸發。

(2)描述屬性可觀察性的操作符

Mobx 中的 Modifiers 可理解爲描述屬性可觀察性的操作符,被用來在定義可觀察屬性時,改變某些屬性的自動轉換規則。

在定義領域模型的可觀察屬性時,有如下三類操作符值得關注:

  • observable.deep

    deep 操作符是默認操作符,它會遞歸的將所有屬性都轉換爲可觀察屬性。通常情況下,這是一個非常便利的方式,無需更多操作即可將定義的屬性進行深度的轉換。

  • observable.ref

    ref 操作符表示觀察的是對象的引用關係,而不關注對象自身的變更。

    例如,我們爲 Contact 類增加 address 屬性,值爲另一個領域模型 Address 的實例對象。通過使用 ref 修飾符,在 address 實例對象的屬性變更時,contact 對象不會被觸發更新,而當 address 屬性被修改爲新的 address 實例對象,因爲引用關係變更,contact 對象被觸發更新。

    let address = new Address();
    class Contact {
        ...
        @observable.ref address = address;
    }
    let contact = new Contact();
    address.city = 'New York'; //不會觸發更新通知
    contact.address = new Address();//引用關係變更,觸發更新通知
  • observable.shallow

    shallow 操作符表示對該屬性進行一個淺觀察,通常用於描述數組類型屬性。shallow 是與 deep 相對的概念,它不會遞歸的將子屬性轉換爲可觀察對象。

    let plainObj = {key:'test'};
    class Contact {
        ...
        @observable.shallow arr = [];
    }
    let contact = new Contact();
    contact.arr.push(plainObj); //plainObj還是一個plainObj
    //如果去掉shallow修飾符,plainObj被遞歸轉換爲observable object

當我們對 Mobx 的使用逐漸深入,應當再次檢查項目中應用狀態的設計,合理地使用這些操作符來限制可觀察性,對於提升應用性能有積極意義。

修改應用狀態

在 Mobx 中修改應用狀態是一件很簡單的事情,在前文的示例中,我們直接修改領域模型實例對象的屬性值來變更應用狀態。

class Contact {
    @observable firstName = 'han;
}
let contact = new Contact();
contact.firstName = 'li';

像這樣修改應用狀態很便捷,但是會來帶兩個問題:

  • 需要修改多個屬性時,每次修改均會觸發相關依賴的更新;
  • 對應用狀態的修改分散在項目多個地方,不便於跟蹤狀態變化,降低可維護性。

爲解決上述問題,Mobx 引入了 action。在我們的使用中,建議通過設置 useStrict(true),使用 action 作爲修改應用狀態的唯一入口。

class Contact {
    @observable firstName = 'han';
    @observable lastName = "meimei";
    @action changeName(first, last) {
        this.firstName = first;
        this.lastName = last;
    }
}
let contact = new Contact();
contact.changeName('li', 'lei');

採用 @action 修飾符,狀態修改方法被包裹在一個事務中,對多個屬性的變更變成了一個原子操作,僅在方法結束時,Mobx 纔會觸發一次對相關依賴的更新通知。與此同時,所有對狀態的修改都統一到應用狀態的指定標識的方法中,一方面提升了代碼可維護性,另一方面,也便於調試工具提供有效的調試信息。

需要注意的是 action 只能影響當前函數作用域,函數中如果有異步調用並且在異步請求返回時需要修改應用狀態,則需要對異步調用也使用 aciton 包裹。當使用 async/await 語法處理異步請求時,可以使用 runInAction 來包裹你的異步狀態修改過程。

class Contact {
    @observable title ;
    @action getTitle() {
        this.pendingRequestCount++;
        fetch(url).then(action(resp => {
            this.title = resp.title;
            this.pendingRequestCount--;
        }))
    }

    @action getTitleAsync = async () => {
        this.pendingRequestCount++;
        const data = await fetchDataFromUrl(url);
        runInAction("update state after fetching data", () => {
            this.title = data.title;
            this.pendingRequestCount--;
        })
    }
}

上述是示例中包含了在 Mobx action 中處理異步請求的過程,這一過程與我們在普通 JavaScript 方法中處理異步請求基本一致,唯一的差別,是對應用狀態的更新需要用 action 包裹。

響應狀態變更

在 Mobx action 中更新應用狀態時,Mobx 自動的將變更通知到相關依賴的部分,我們僅需關注如何響應變更。Mobx 中有多種響應變更的方法,包括 autorun、reaction、when 等,本節探討其使用場景。

(1)autorun

autorun 是Mobx中最常用的觀察者,當你需要根據依賴自動的運行一個方法,而不是產生一個新值,可以使用 autorun 來實現這一效果。

class Contact {
    @observable firstName = 'Han';
    @observable lastName = "meimei";
    constructor() {
        autorun(()=>{
            console.log(`Name changed: ${this.firstName}, ${this.lastName}`);
        });
        this.firstName = 'Li';
    }
}

// Name changed: Han, meimei
// Name changed: Li, meimei
(2)reaction

從上例輸出的日誌可以看出,autorun 在定義時會立即執行,之後在依賴的屬性變更時,會重新執行。如果我們希望僅在依賴狀態變更時,才執行方法,可以使用 reaction。

reaction 可以如下定義:

reaction = tracking function + effect function

其使用方式如下所示:

reaction(() => data, data => { sideEffect }, options?)

函數定義中,第一個參數即爲 tracking function,它返回需要被觀察的數據。這個數據被傳入第二個參數即 effect function,在 effect function 中處理邏輯,產生副作用。

在定義 reaction 方法時,effect function 不會立即執行。僅當 tracking function 返回的數據變更時,纔會觸發 effect function 的執行。通過將 autorun 拆分爲 tracking function 和 effect function,我們可以對監聽響應進行更細粒度的控制。

(3) when

autorun 和reaction 可以視爲長期運行的觀察者,如果不調用銷燬方法,他們會在應用的整個生命週期內有效。如果我們僅需在特定條件下執行一次目標方法,可以使用 when。

when 的使用方法如下所示:

when(debugName?, predicate: () => boolean, effect: () => void, scope?)

與 reaction 類似,when 主要參數有兩個,第一個是 tracking function,返回一個布爾值,僅當布爾值爲 true 時,第二個參數即 effect function 會被觸發執行。在執行完成後,Mobx 會自動銷燬這一觀察者,無需手動處理。

在應用中使用

Mobx 是一個獨立的應用狀態管理工具,可以應用在多種架構的項目中。當我們在 React 項目中使用 Mobx 時,使用 mobx-react 工具庫可以幫助我們更便利的使用。

Mobx 中應用狀態分散在各個領域模型中,一個領域模型可視爲一個 store。在我們的實際使用中,當應用規模逐漸複雜,會遇到這樣的問題:

當我們需要在一個 store 中使用或更新其他 store 狀態數據時,應當如何處理呢?

Mobx 並不提供這方面的意見,它允許你自由的組織你的代碼結構。但是在面臨這一場景時,如果僅是互相引用 store,最終會將應用狀態互相耦合,多個 store 被混合成一個整體,代碼的可維護性降低。

爲我們帶來這一困擾的原因,是因爲 action 處於某一 store 中,而其本身除了處理應用狀態修改之外,還承載了業務邏輯如異步請求的處理過程。實際上,這違背了單一職責的設計原則,爲解決這一問題,我們將業務邏輯抽離,混合使用了 Redux action creator 的結構。

import contactStore from '../../stores/contactStore';
import messageStore from '../../stores/messageStore';

export function syncContactAndMessageFromServer(url) {
  const requestType = requestTypes.SYNC_DATA;
  if (requestStore.getRequestByType(requestType)) { return; }
  requestStore.setRequestInProcess(requestType, true);
  return fetch(url)
    .then(response => response.json())
    .then(data => {
      contactStore.setContacts(data.contacts);
      messageStore.setMessages(data.messages);
      requestStore.setRequestInProcess(requestType, false);
    });
}

上述示例中,我們將業務邏輯抽離,在這一層自由的引用所需的 Mobx store,並在業務邏輯處理結束,調用各自的 action 方法去修改應用狀態。

這種解決方案結合了 Redux action creator 思路,引入了單獨的業務邏輯層。如果不喜歡這一方式,也可以採用另一種思路來重構我們的 store。

在這種方法裏,我們將 store 與 action 拆分,在 store 中僅保留屬性定義,在 action 中處理業務邏輯和狀態更新,其結構如下所示:

export class ContactStore {
    @observable contacts = [];
    // ...other properties
}

export class MessageStore {
    @observable messages = observable.map({});
    // ...other properties
}

export MainActions {
    constructor(contactStore,messageStore) {
        this.contactStore = contactStore,
        this.messageStore = messageStore
    }
    @action syncContactAndMessageFromServer(url) {
        ...
    }
}

兩種方法的均能解決問題,可以看出第二種方法對 store 原結構改動較大,在我們的實際開發中,使用第一種方法。

未完待續……

如何選擇你的狀態管理方案?Redux、Mobx 關鍵差異有哪些?選擇方案時應做出哪些考慮?提前閱讀完整版?歡迎掃碼,關注“CSDN前端開發者說”公衆號,暢快閱讀全文!

圖片描述

本文選自《程序員》7期“前端開發創新實踐”特別專題。本期雜誌即將上市,歡迎訂閱


2017年7月8日(星期六),「“前端開發創新實踐”線上峯會」將在 CSDN 學院召開。本次峯會集結來自Smashing Magazine、美國Hulu、美團、廣發證券、去哪兒網、百度的多位國內外知名前端開發專家、資深架構師,主題涵蓋響應式佈局、Redux、Mobx、狀態管理、構建方案、代碼複用、個性化圖表定製度等前端開發重難點技術話題。技術解析加項目實戰,幫你開拓解決問題的思路,增強技術探索實踐能力。全天六場深度技術分享,現在僅需169元,限時優惠中,詳情點擊峯會官網

圖片描述

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