Redux PK Mobx前域,誰更適合管理大規(guī)模應用的前端狀態(tài)辕近?

本文首發(fā)于:CSDN「前端開發(fā)者說」公眾號
CSDN「前端開發(fā)者說」公眾號(ID:bigfrontend)匿垄,專注前端開發(fā)領域移宅,播報熱點新聞、共享同行高手鉆研成果椿疗、展現(xiàn)企業(yè)最佳實踐及研發(fā)歷程漏峰,幫廣大前端開發(fā)者走好技術成長中的每一步。

作者:龔麒届榄,廣發(fā)證券信息技術部高級前端工程師浅乔,負責廣發(fā)證券金鑰匙項目的React Native改造,及廣發(fā)證券有問必答業(yè)務的微信小程序版的開發(fā)铝条,在混合開發(fā)和各類跨端解決方案應用上具有豐富經(jīng)驗靖苇。

前言

前端的發(fā)展日新月異,Angular班缰、React贤壁、Vue 等前端框架的興起,為我們的應用開發(fā)帶來新的體驗埠忘。React Native脾拆、Weex、微信小程序等技術方案的出現(xiàn)给梅,又進一步擴展了前端技術的應用范圍假丧。隨著這些技術的革新,我們可以更加便利地編寫更高復雜度动羽、更大規(guī)模的應用包帚,而在這一過程中,如何優(yōu)雅的管理應用中的數(shù)據(jù)狀態(tài)成為了一個需要解決的問題运吓。

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

Dive into Redux

Redux 是什么

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

(state, action) => newState

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

  • 單一數(shù)據(jù)源

    在 Redux 中瓮床,整個應用的狀態(tài)以狀態(tài)樹的形式,被存儲在一個單例的 store 中。

  • 狀態(tài)數(shù)據(jù)只讀

    惟一改變狀態(tài)數(shù)據(jù)的方法是觸發(fā) action隘庄,action 是一個用于描述已發(fā)生事件的普通對象踢步。

  • 使用純函數(shù)修改狀態(tài)

    在 Redux 中,通過純函數(shù)丑掺,即 reducer 來定義如何修改 state获印。

從上述原則中,可以看出街州,構成 Redux 的主要元素有 action兼丰、reducer、store唆缴,借用一張經(jīng)典圖示(見圖1)鳍征,可以進一步理解 Redux 主要元素和數(shù)據(jù)流向。

圖1  展示了Redux的主要元素和數(shù)據(jù)流向
圖1 展示了Redux的主要元素和數(shù)據(jù)流向

探索 The Redux way

異步方案選型

Redux 中通過觸發(fā) action 修改狀態(tài)面徽,而 action 是一個普通對象蟆技,action creator 是一個純函數(shù)。如何在 action creator 中融入斗忌、管理我們的異步請求,是在實際開發(fā)中首先需要解決的問題旺聚。

當前 Redux 生態(tài)活躍织阳,出現(xiàn)了不少異步管理中間件。在我們的實踐中砰粹,認為大致可以分為兩類唧躲。

(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 后碱璃,我們將異步處理和業(yè)務邏輯定義在一個方法中弄痹,利用中間件機制,將方法的執(zhí)行交由中間件管理嵌器。

上例是一個簡單的異步請求肛真,在代碼中我們需要主動地根據(jù)異步請求的執(zhí)行狀態(tài),分別觸發(fā)請求開始爽航、成功和失敗三個 action蚓让。這一過程顯得繁瑣,當應用中有大量這類簡單請求時讥珍,項目中會充滿這種重復代碼历极。

針對這一問題,出現(xiàn)了一些用于簡化這類簡單請求的工具衷佃。實際開發(fā)中趟卸,我們選擇了 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 結(jié)構一致的普通對象锄列,不同的是图云,payload 屬性是一個返回 Promise 對象的異步方法。通過將異步方法的執(zhí)行過程交由 redux-promise-middleware 中間件處理右蕊,中間件會幫助我們處理異步請求的狀態(tài)琼稻,根據(jù)異步請求的結(jié)果為當前操作類型添加 PEDNGING/FULFILLED/REJECTED 狀態(tài),我們的代碼得到大幅簡化饶囚。

redux-promise-middleware 中間件適用于簡化簡單請求的代碼帕翻,開發(fā)中推薦混合使用 redux-promise-middleware 中間件和 redux-thunk。

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

以 redux-thunk 為代表的中間件可以滿足一般的業(yè)務場景萝风,但當業(yè)務對用戶事件嘀掸、異步請求有更細粒度的控制需求時,redux-thunk 不能便利的滿足规惰。此時睬塌,可以選擇以 redux-saga 為代表的中間件。

redux-saga 可以理解為一個和系統(tǒng)交互的常駐進程歇万,其中揩晴,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 經(jīng)由 root saga 分發(fā);
  • 具體業(yè)務處理方法中刹前,通過提供的 call/put 等幫助方法泳赋,聲明式的進行方法調(diào)用;
  • 使用 ES6 Generator 語法喇喉,簡化異步代碼語法祖今。

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

簡單列舉如下:

  • 提供 takeLatest/takeEvery/throttle 方法大渤,可以便利的實現(xiàn)對事件的僅關注最近事件、關注每一次掸绞、事件限頻泵三;
  • 提供 cancel/delay 方法耕捞,可以便利的取消、延遲異步請求烫幕;
  • 提供 race(effects),[…effects] 方法來支持競態(tài)和并行場景俺抽;
  • 提供 channel 機制支持外部事件。

在 Redux 生態(tài)中较曼,除了 redux-saga 中間件磷斧,還有另一個中間件,redux-observable 也可以滿足這一場景捷犹。

redux-observable 是基于 RxJS 的用于處理異步請求的中間件弛饭,借助 RxJS 的各種操作符和幫助方法,redux-observable 也能實現(xiàn)對各類事件的細粒度操作萍歉,比如取消侣颂、限頻、延遲請求等枪孩。

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

應用狀態(tài)設計

如何設計應用狀態(tài)的數(shù)據(jù)結(jié)構是一個值得思考的問題,在實踐中攻询,我們總結(jié)了兩點數(shù)據(jù)劃分的指導性原則澎蛛,應用狀態(tài)扁平化和抽離公共狀態(tài)。

(1) 應用狀態(tài)扁平化

在我們的項目中蜕窿,有聯(lián)系人、聊天消息和當前聯(lián)系人對象呆馁。最初我們采用如下數(shù)據(jù)結(jié)構:

{
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'
            },
            ...
        ]
    }
}

采用上述數(shù)據(jù)機構桐经,帶來幾個問題。

  • 消息對象與聯(lián)系人對象耦合浙滤,消息對象的變更操作引發(fā)聯(lián)系人對象的變更操作阴挣;
  • 聯(lián)系人集合和當前聯(lián)系人對象數(shù)據(jù)冗余,當數(shù)據(jù)更新時需要多處修改來保持數(shù)據(jù)一致性纺腊;
  • 數(shù)據(jù)結(jié)構嵌套過深畔咧,不便于數(shù)據(jù)更新,一定程度上導致更新時的耗時增加揖膜。

將數(shù)據(jù)扁平化誓沸、解除耦合,得到如下數(shù)據(jù)結(jié)構:

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

相對于之前的問題壹粟,上述數(shù)據(jù)結(jié)構具有以下優(yōu)點:

  • 細粒度的更新數(shù)據(jù)拜隧,進而細粒度控制視圖的渲染;
  • 結(jié)構清晰,避免更新數(shù)據(jù)時洪添,復雜的數(shù)據(jù)操作垦页;
  • 去除冗余數(shù)據(jù),避免數(shù)據(jù)不一致干奢。
(2)抽離公共狀態(tài)

在領域?qū)ο笾馊福€有另外一些與請求過程相關的狀態(tài)數(shù)據(jù),如下所示:

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

上述數(shù)據(jù)結(jié)構中忿峻,我們按照功能模塊將狀態(tài)數(shù)據(jù)內(nèi)聚薄啥。采用上述結(jié)構,會導致我們需要寫很多基本重復的 action炭菌,如下所示:

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

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

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

我們分別為 user 罪佳、message 、author 定義了一系列 action黑低,它們作用類似赘艳,代碼重復。為解決這一問題克握,我們可以將這類狀態(tài)數(shù)據(jù)抽離蕾管,不再簡單的按照功能模塊內(nèi)聚,抽離后的狀態(tài)數(shù)據(jù)如下所示:

{
  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: { ... },
  },
}

采用這一結(jié)構菩暗,可以避免定義大量相似的 action type掰曾,避免編寫重復的 action。

修改狀態(tài)數(shù)據(jù)

將應用狀態(tài)數(shù)據(jù)不可變化是使用 Redux 的一般范式停团,有多種方式可以實現(xiàn)不可變數(shù)據(jù)的效果旷坦,我們分別嘗試了 immutable.js 和 seamless-immutable.js,并在實際開發(fā)中選擇了 seamless-immutable.js佑稠。

(1)immutable.js

immutable.js 是一個知名度很高的不可變數(shù)據(jù)實現(xiàn)庫秒梅。它為人稱道的是基于共享數(shù)據(jù)結(jié)構所帶來的數(shù)據(jù)修改時的高性能,但是在我們的使用過程中舌胶,發(fā)現(xiàn)其易用性不夠友好被饿,使用體驗并不美好纵势。

  • 首先盗扒,immutable.js 實現(xiàn)的是 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 使用了自定義的數(shù)據(jù)結(jié)構,這意味著貫穿我們的應用都需要明確當前使用的是 immutable.js 的數(shù)據(jù)結(jié)構切心。獲取數(shù)據(jù)時洞辣,需要使用 get 方法咐刨,而不能使用 obj.prop 或者 obj[prop]。在需要將數(shù)據(jù)同外部交互扬霜,如存儲或者請求時定鸟,需要將特有數(shù)據(jù)結(jié)構轉(zhuǎn)換成原生 JavasScript 對象。

  • 最后著瓶,以 state.set('key',obj) 形式更新狀態(tài)時联予,obj 對象不能自動的 immutable 化。

(2)seamless-immutable.js

上述問題使得我們在開發(fā)中不斷地需要停下來思考當前寫法是否正確材原,于是我們繼續(xù)嘗試沸久,最后選擇使用 seamless-immutable.js 來幫助實現(xiàn)不可變數(shù)據(jù)。

seamless-immutable.js 意為無縫的 immutable余蟹,與 immutable.js 不同卷胯,它沒有定義新的數(shù)據(jù)結(jié)構,其基本使用如下所示:

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' }

根據(jù)我們的使用體驗威酒,seamless-immutable.js 易用性優(yōu)于 immutable.js窑睁。但是在選擇之前,有一點需要了解的是葵孤,在數(shù)據(jù)修改時担钮,seamless-immutable.js 性能低于 immutable.js。數(shù)據(jù)嵌套層級越深尤仍,數(shù)據(jù)量越大箫津,性能差異越明顯。這里需要根據(jù)業(yè)務特點來做選擇宰啦,我們的業(yè)務沒有大批量的深度數(shù)據(jù)修改需求苏遥,易用性比性能更重要。

在應用中使用

Redux 可以應用在多種場景赡模,在我們的開發(fā)中暖眼,已經(jīng)將它應用到了 React Native、Angular 1.x 重構和微信小程序的項目上纺裁。

在前文介紹 Redux 三原則時提到,Redux 具有單一數(shù)據(jù)源司澎,觸發(fā) action 時,Redux store 在執(zhí)行狀態(tài)更新邏輯后欺缘,會執(zhí)行注冊在 store 上的事件處理函數(shù)。

基于上述過程挤安,在簡單的 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();

結(jié)合前端框架使用 Redux 時谚殊,社區(qū)中已經(jīng)有了 react-redux、ng-redux 這類的幫助工具蛤铜,甚至對應微信小程序嫩絮,也有了類似的實現(xiàn)方案丛肢。其實現(xiàn)原理均一致,都是通過全局對象綁定 Redux store,使得在應用組件中可以獲得 store 中的狀態(tài)數(shù)據(jù)剿干,并向 store 注冊事件處理函數(shù)蜂怎,用來在狀態(tài)變更時觸發(fā)視圖的更新。

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

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

嚴格遵循這一模式并無不可,但是當項目規(guī)模逐漸擴大榜轿,文件數(shù)量增多后幽歼,切換多文件夾尋找文件變得有些繁瑣,在這一時刻谬盐,可以考慮嘗試 Redux Ducks 模式甸私。

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

所謂 Ducks 模式,也即經(jīng)典的鴨子類型飞傀。這里將同一領域內(nèi)皇型,Redux 相關元素的文件合并至同一個文件 userRedux 中,可以避免為實現(xiàn)一個簡單功能頻繁在不同目錄切換文件助析。

與此同時犀被,根據(jù)我們的使用經(jīng)驗,鴨子模式與傳統(tǒng)模式應當靈活的混合使用外冀。當業(yè)務邏輯復雜寡键,action與reducer各自代碼量較多時,按照傳統(tǒng)模式拆分可能是更好的選擇雪隧。此時可以如下混合使用兩種模式:

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

Dive into Mobx

Mobx 是什么

Mobx 是一個簡單西轩、可擴展的前端應用狀態(tài)管理工具。Mobx 背后的哲學很簡單:當應用狀態(tài)更新時脑沿,所有依賴于這些應用狀態(tài)的觀察者(包括UI藕畔、服務端數(shù)據(jù)同步函數(shù)等),都應該自動得到細粒度地更新庄拇。

Mobx 中主要包含如下元素:

  • State

    State 是被觀察的應用狀態(tài)注服,狀態(tài)是驅(qū)動你的應用的數(shù)據(jù)。

  • Derivations

    Derivations 可以理解為衍生措近,它是應用狀態(tài)的觀察者溶弟。Mobx 中有兩種形式的衍生,分別是 Computed values 和 Reactions瞭郑。其中辜御,Computed values 是計算屬性,它的數(shù)據(jù)通過純函數(shù)由應用狀態(tài)計算得來屈张,當依賴的應用狀態(tài)變更時擒权,Mobx 自動觸發(fā)計算屬性的更新袱巨。Reactions 可簡單理解為響應,與計算屬性類似碳抄,它響應所依賴的應用狀態(tài)的變更愉老,不同的是,它不產(chǎn)生新的數(shù)據(jù)纳鼎,而是輸出相應的副作用(side effects)俺夕,比如更新UI。

  • Actions

    Actions 可以理解為動作贱鄙,由應用中的各類事件觸發(fā)劝贸。Actions 是變更應用狀態(tài)的地方,可以幫助你更加清晰的組織你的代碼逗宁。

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

圖2 Mobx項目主頁的示例圖
圖2 Mobx項目主頁的示例圖

探索 The Mobx Way

在探索 Redux 的過程中,我們關注異步方案選型瞎颗、應用狀態(tài)設計件甥、如何修改狀態(tài)以及怎樣在應用中使用。當我們走進 Mobx哼拔,探索 Mobx 的應用之道時引有,分別從應用狀態(tài)設計、變更應用狀態(tài)倦逐、響應狀態(tài)變更以及如何在實際譬正、復雜項目中應用進行了思考。

應用狀態(tài)設計

設計應用狀態(tài)是開始使用 Mobx 的第一步檬姥,讓我們開始設計應用狀態(tài):

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 修飾符定義被觀察的屬性抒巢。

領域模型組成了應用的狀態(tài),定義領域模型的可觀察屬性是 Mobx 中應用狀態(tài)設計的關鍵步驟秉犹。在這一過程中蛉谜,我們需要關注兩方面內(nèi)容,分別是 Mobx 數(shù)據(jù)類型和描述屬性可觀察性的操作符崇堵。

(1)Mobx 數(shù)據(jù)類型

Mobx 內(nèi)置了幾種數(shù)據(jù)類型型诚,包括 objects、arrays筑辨、maps 和 box values。

  • objects 是 Mobx 中最常見的對象幸逆,示例中 Contact 類的實例對象棍辕,或者通過 mobx.observable({ key:value}) 定義的對象均為 Observable Objects暮现。
  • box values 相對來說使用較少,它可以將 JavaScript 中的基本類型如字符串轉(zhuǎn)為可觀察對象楚昭。
  • arrays 和maps 是 Mobx 對 JavaScript 原生 Array 和 Map 的封裝栖袋,用于實現(xiàn)對更復雜數(shù)據(jù)結(jié)構的監(jiān)聽。

當使用 Mobx arrays 結(jié)構時抚太,有一個需要注意的地方塘幅,如下所示,經(jīng)封裝后尿贫,它不再是一個原生 Array 類型了电媳。

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

這是一個我們最初使用時,容易走進的陷阱庆亡。當需要將一個 observable array 與第三方庫交互使用時匾乓,可以對它創(chuàng)建一份淺復制,像下面這樣又谋,轉(zhuǎn)為原生 JavaScript:

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

默認情況下拼缝,領域模型中被預定義為可觀察的屬性才能被監(jiān)聽,而為實例對象新增的屬性彰亥,不能被自動觀察咧七。使用 Mobx maps,即使新增的屬性也是可觀察的任斋,我們不僅可以響應集合中某一元素的變更继阻,也能響應新增、刪除元素這些操作仁卷。

除了使用上述幾種數(shù)據(jù)類型來定義可觀察的屬性穴翩,還有一個很常用的概念,計算屬性锦积。通常芒帕,計算屬性不是領域模型中的真實屬性,而是依賴其他屬性計算得來丰介。系統(tǒng)收集它對其他屬性的依賴關系背蟆,僅當依賴屬性變更時,計算屬性的重新計算才會被觸發(fā)哮幢。

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

Mobx 中的 Modifiers 可理解為描述屬性可觀察性的操作符带膀,被用來在定義可觀察屬性時,改變某些屬性的自動轉(zhuǎn)換規(guī)則橙垢。

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

  • observable.deep

    deep 操作符是默認操作符,它會遞歸的將所有屬性都轉(zhuǎn)換為可觀察屬性柜某。通常情況下嗽元,這是一個非常便利的方式敛纲,無需更多操作即可將定義的屬性進行深度的轉(zhuǎn)換。

  • observable.ref

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

    例如,我們?yōu)?Contact 類增加 address 屬性佩谷,值為另一個領域模型 Address 的實例對象旁壮。通過使用 ref 修飾符,在 address 實例對象的屬性變更時谐檀,contact 對象不會被觸發(fā)更新抡谐,而當 address 屬性被修改為新的 address 實例對象,因為引用關系變更稚补,contact 對象被觸發(fā)更新童叠。

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

    shallow 操作符表示對該屬性進行一個淺觀察,通常用于描述數(shù)組類型屬性课幕。shallow 是與 deep 相對的概念厦坛,它不會遞歸的將子屬性轉(zhuǎn)換為可觀察對象。

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

當我們對 Mobx 的使用逐漸深入杜秸,應當再次檢查項目中應用狀態(tài)的設計,合理地使用這些操作符來限制可觀察性润绎,對于提升應用性能有積極意義撬碟。

修改應用狀態(tài)

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

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

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

  • 需要修改多個屬性時棍郎,每次修改均會觸發(fā)相關依賴的更新其障;
  • 對應用狀態(tài)的修改分散在項目多個地方,不便于跟蹤狀態(tài)變化涂佃,降低可維護性励翼。

為解決上述問題,Mobx 引入了 action辜荠。在我們的使用中汽抚,建議通過設置 useStrict(true),使用 action 作為修改應用狀態(tài)的唯一入口伯病。

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 修飾符造烁,狀態(tài)修改方法被包裹在一個事務中,對多個屬性的變更變成了一個原子操作,僅在方法結(jié)束時惭蟋,Mobx 才會觸發(fā)一次對相關依賴的更新通知叠纹。與此同時,所有對狀態(tài)的修改都統(tǒng)一到應用狀態(tài)的指定標識的方法中敞葛,一方面提升了代碼可維護性,另一方面与涡,也便于調(diào)試工具提供有效的調(diào)試信息惹谐。

需要注意的是 action 只能影響當前函數(shù)作用域,函數(shù)中如果有異步調(diào)用并且在異步請求返回時需要修改應用狀態(tài)驼卖,則需要對異步調(diào)用也使用 aciton 包裹氨肌。當使用 async/await 語法處理異步請求時,可以使用 runInAction 來包裹你的異步狀態(tài)修改過程酌畜。

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 方法中處理異步請求基本一致,唯一的差別桥胞,是對應用狀態(tài)的更新需要用 action 包裹恳守。

響應狀態(tài)變更

在 Mobx action 中更新應用狀態(tài)時,Mobx 自動的將變更通知到相關依賴的部分贩虾,我們僅需關注如何響應變更催烘。Mobx 中有多種響應變更的方法,包括 autorun缎罢、reaction伊群、when 等,本節(jié)探討其使用場景策精。

(1)autorun

autorun 是Mobx中最常用的觀察者舰始,當你需要根據(jù)依賴自動的運行一個方法,而不是產(chǎn)生一個新值咽袜,可以使用 autorun 來實現(xiàn)這一效果丸卷。

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 在定義時會立即執(zhí)行酬蹋,之后在依賴的屬性變更時及老,會重新執(zhí)行。如果我們希望僅在依賴狀態(tài)變更時范抓,才執(zhí)行方法骄恶,可以使用 reaction。

reaction 可以如下定義:

reaction = tracking function + effect function

其使用方式如下所示:

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

函數(shù)定義中匕垫,第一個參數(shù)即為 tracking function僧鲁,它返回需要被觀察的數(shù)據(jù)。這個數(shù)據(jù)被傳入第二個參數(shù)即 effect function,在 effect function 中處理邏輯寞秃,產(chǎn)生副作用斟叼。

在定義 reaction 方法時,effect function 不會立即執(zhí)行春寿。僅當 tracking function 返回的數(shù)據(jù)變更時朗涩,才會觸發(fā) effect function 的執(zhí)行。通過將 autorun 拆分為 tracking function 和 effect function绑改,我們可以對監(jiān)聽響應進行更細粒度的控制谢床。

(3) when

autorun 和reaction 可以視為長期運行的觀察者,如果不調(diào)用銷毀方法厘线,他們會在應用的整個生命周期內(nèi)有效识腿。如果我們僅需在特定條件下執(zhí)行一次目標方法,可以使用 when造壮。

when 的使用方法如下所示:

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

與 reaction 類似渡讼,when 主要參數(shù)有兩個,第一個是 tracking function耳璧,返回一個布爾值成箫,僅當布爾值為 true 時,第二個參數(shù)即 effect function 會被觸發(fā)執(zhí)行旨枯。在執(zhí)行完成后伟众,Mobx 會自動銷毀這一觀察者,無需手動處理召廷。

在應用中使用

Mobx 是一個獨立的應用狀態(tài)管理工具凳厢,可以應用在多種架構的項目中。當我們在 React 項目中使用 Mobx 時竞慢,使用 mobx-react 工具庫可以幫助我們更便利的使用先紫。

Mobx 中應用狀態(tài)分散在各個領域模型中,一個領域模型可視為一個 store筹煮。在我們的實際使用中遮精,當應用規(guī)模逐漸復雜,會遇到這樣的問題:

當我們需要在一個 store 中使用或更新其他 store 狀態(tài)數(shù)據(jù)時败潦,應當如何處理呢本冲?

Mobx 并不提供這方面的意見,它允許你自由的組織你的代碼結(jié)構劫扒。但是在面臨這一場景時檬洞,如果僅是互相引用 store,最終會將應用狀態(tài)互相耦合沟饥,多個 store 被混合成一個整體添怔,代碼的可維護性降低湾戳。

為我們帶來這一困擾的原因,是因為 action 處于某一 store 中广料,而其本身除了處理應用狀態(tài)修改之外砾脑,還承載了業(yè)務邏輯如異步請求的處理過程。實際上艾杏,這違背了單一職責的設計原則韧衣,為解決這一問題,我們將業(yè)務邏輯抽離购桑,混合使用了 Redux action creator 的結(jié)構汹族。

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);
    });
}

上述示例中,我們將業(yè)務邏輯抽離其兴,在這一層自由的引用所需的 Mobx store,并在業(yè)務邏輯處理結(jié)束夸政,調(diào)用各自的 action 方法去修改應用狀態(tài)元旬。

這種解決方案結(jié)合了 Redux action creator 思路,引入了單獨的業(yè)務邏輯層守问。如果不喜歡這一方式匀归,也可以采用另一種思路來重構我們的 store。

在這種方法里耗帕,我們將 store 與 action 拆分穆端,在 store 中僅保留屬性定義,在 action 中處理業(yè)務邏輯和狀態(tài)更新仿便,其結(jié)構如下所示:

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 原結(jié)構改動較大,在我們的實際開發(fā)中嗽仪,使用第一種方法荒勇。

未完待續(xù)……

如何選擇你的狀態(tài)管理方案?Redux闻坚、Mobx 關鍵差異有哪些沽翔?選擇方案時應做出哪些考慮?提前閱讀完整版窿凤?歡迎掃碼仅偎,關注“CSDN前端開發(fā)者說”公眾號,暢快閱讀全文雳殊!

圖片描述
圖片描述

本文選自《程序員》7期“前端開發(fā)創(chuàng)新實踐”特別專題橘沥。本期雜志即將上市,歡迎訂閱夯秃。 投稿請聯(lián)系:chenqg@csdn.net威恼。


同時告訴大家一個好消息品姓。2017 年 7 月 8 日(星期六),「“CSDN前端開發(fā)”在線峰會」將在 CSDN 學院召開箫措,本文作者龔麒已受邀參加腹备,并圍繞前端狀態(tài)管理,深度分享廣發(fā)證券在該領域做出的一系列探索與實踐斤蔓。屆時植酥,您將了解當前最為火熱的兩款前端狀態(tài)管理工具,學會評析不同狀態(tài)管理方案的弦牡,并在選型時做出合理取舍友驮。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驾锰,隨后出現(xiàn)的幾起案子卸留,更是在濱河造成了極大的恐慌,老刑警劉巖椭豫,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耻瑟,死亡現(xiàn)場離奇詭異,居然都是意外死亡赏酥,警方通過查閱死者的電腦和手機喳整,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裸扶,“玉大人框都,你說我怎么就攤上這事『浅浚” “怎么了魏保?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長摸屠。 經(jīng)常有香客問我囱淋,道長,這世上最難降的妖魔是什么餐塘? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任妥衣,我火速辦了婚禮,結(jié)果婚禮上戒傻,老公的妹妹穿的比我還像新娘税手。我一直安慰自己,他們只是感情好需纳,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布芦倒。 她就那樣靜靜地躺著,像睡著了一般不翩。 火紅的嫁衣襯著肌膚如雪兵扬。 梳的紋絲不亂的頭發(fā)上麻裳,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音器钟,去河邊找鬼津坑。 笑死,一個胖子當著我的面吹牛傲霸,可吹牛的內(nèi)容都是我干的疆瑰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼昙啄,長吁一口氣:“原來是場噩夢啊……” “哼穆役!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梳凛,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤耿币,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后韧拒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淹接,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年叭莫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烁试。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡雇初,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出减响,到底是詐尸還是另有隱情靖诗,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布支示,位于F島的核電站刊橘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏颂鸿。R本人自食惡果不足惜促绵,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘴纺。 院中可真熱鬧败晴,春花似錦、人聲如沸栽渴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闲擦。三九已至慢味,卻和暖如春场梆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纯路。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工或油, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人感昼。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓装哆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親定嗓。 傳聞我的和親對象是個殘疾皇子蜕琴,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容