你需要Mobx還是Redux?

前端魔法師發表於2018-02-11

在過去一年,越來越多的專案繼續或者開始使用React和Redux開發,這是目前前端業內很普遍的一種前端專案解決方案,但是隨著開發專案越來越多,越來越多樣化時,個人又有了不同的感受和想法。是不是因為已經有了一個比較普遍的,熟悉的專案技術棧,我們就一直完全沿用呢,有沒有比他更適合的方案呢?恰逢團隊最近有一個新專案,於是博主開始思考,有沒有可能使用其他可替代技術開發呢?既能提高開發效率,又能擴充技術儲備和眼界,經過調研,選擇了Mobx,最終使用React+Mobx搭建了新專案,本篇總結分享從技術選型到專案實現的較完整過程,希望共同進步。

歡迎訪問我的個人部落格

前言

當我們使用React開發web應用程式時,在React元件內,可以使用this.setState()this.state處理或訪問元件內狀態,但是隨著專案變大,狀態變複雜,通常需要考慮元件間通訊問題,主要包括以下兩點:

  1. 某一個狀態需要在多個元件間共享(訪問,更新);
  2. 某元件內互動需要觸發其他元件的狀態更新;

關於這些問題,React元件開發實踐推薦將公用元件狀態提升:

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor

通常多元件需要處理同一狀態,我們推薦將共享狀態提升至他們的共同最近祖先元件內。更多詳情檢視

當專案越發複雜時,我們發現僅僅是提升狀態已經無法適應如此複雜的狀態管理了,程式狀態變得比較難同步,操作,到處是回撥,釋出,訂閱,這意味著我們需要更好的狀態管理方式,於是就引入了狀態管理庫,如ReduxMobxJumpsuitAlt.js等。

狀態管理

狀態管理庫,無論是Redux,還是Mobx這些,其本質都是為了解決狀態管理混亂,無法有效同步的問題,它們都支援:

  1. 統一維護管理應用狀態;
  2. 某一狀態只有一個可信資料來源(通常命名為store,指狀態容器);
  3. 操作更新狀態方式統一,並且可控(通常以action方式提供更新狀態的途徑);
  4. 支援將store與React元件連線,如react-reduxmobx-react;通常使用狀態管理庫後,我們將React元件從業務上劃分為兩類:
    1. 容器元件(Container Components):負責處理具體業務和狀態資料,將業務或狀態處理函式傳入展示型元件;
    2. 展示型元件(Presentation Components):負責展示檢視,檢視互動回撥內呼叫傳入的處理函式;

Mobx VS Redux

目前來看,Redux已是React應用狀態管理庫中的霸主了,而Mobx則是一方諸侯,我們為什麼要選擇Mobx,而不是繼續沿用Redux呢,那就需要比較他們的異同了。

Mobx和Redux都是JavaScript應用狀態管理庫,都適用於React,Angular,VueJs等框架或庫,而不是侷限於某一特定UI庫。

Redux

要介紹Redux,我們就不得不談到Flux了:

Flux is the application architecture that Facebook uses for building client-side web applications.It's more of a pattern rather than a formal framework

Flux是Facebook用來開發客戶端-服務端web應用程式的應用架構,它更多是一種架構模式,而非一個特定框架。詳解Flux

而Redux更多的是遵循Flux模式的一種實現,是一個JavaScript庫,它關注點主要是以下幾方面:

  1. Action:一個JavaScript物件,描述動作相關資訊,主要包含type屬性和payload屬性:
    1. type:action 型別;
    2. payload:負載資料;
  2. Reducer:定義應用狀態如何響應不同動作(action),如何更新狀態;
  3. Store:管理action和reducer及其關係的物件,主要提供以下功能:
    1. 維護應用狀態並支援訪問狀態(getState());
    2. 支援監聽action的分發,更新狀態(dispatch(action));
    3. 支援訂閱store的變更(subscribe(listener));
  4. 非同步流:由於Redux所有對store狀態的變更,都應該通過action觸發,非同步任務(通常都是業務或獲取資料任務)也不例外,而為了不將業務或資料相關的任務混入React元件中,就需要使用其他框架配合管理非同步任務流程,如redux-thunkredux-saga等;

Mobx

Mobx是一個透明函式響應式程式設計(Transparently Functional Reactive Programming,TFRP)的狀態管理庫,它使得狀態管理簡單可伸縮:

Anything that can be derived from the application state, should be derived. Automatically.

任何起源於應用狀態的資料應該自動獲取。

其原理如圖:

Mobx Philosophy

  1. Action:定義改變狀態的動作函式,包括如何變更狀態;

  2. Store:集中管理模組狀態(State)和動作(action);

  3. Derivation(衍生):從應用狀態中派生而出,且沒有任何其他影響的資料,我們稱為derivation(衍生),衍生在以下情況下存在:

    1. 使用者介面;

    2. 衍生資料;

      衍生主要有兩種:

      1. Computed Values(計算值):計算值總是可以使用純函式(pure function)從當前可觀察狀態中獲取;
      2. Reactions(反應):反應指狀態變更時需要自動發生的副作用,這種情況下,我們需要實現其讀寫操作;
import {observable, autorun} from 'mobx';

var todoStore = observable({
    /* some observable state */
    todos: [],

    /* a derived value */
    get completedCount() {
        return this.todos.filter(todo => todo.completed).length;
    }
});

/* a function that observes the state */
autorun(function() {
    console.log("Completed %d of %d items",
        todoStore.completedCount,
        todoStore.todos.length
    );
});

/* ..and some actions that modify the state */
todoStore.todos[0] = {
    title: "Take a walk",
    completed: false
};
// -> synchronously prints: 'Completed 0 of 1 items'

todoStore.todos[0].completed = true;
// -> synchronously prints: 'Completed 1 of 1 items'
複製程式碼

函式式和麵向物件

Redux更多的是遵循函數語言程式設計(Functional Programming, FP)思想,而Mobx則更多從面相物件角度考慮問題。

Redux提倡編寫函式式程式碼,如reducer就是一個純函式(pure function),如下:

(state, action) => {
  return Object.assign({}, state, {
    ...
  })
}
複製程式碼

純函式,接受輸入,然後輸出結果,除此之外不會有任何影響,也包括不會影響接收的引數;對於相同的輸入總是輸出相同的結果。

Mobx設計更多偏向於物件導向程式設計(OOP)和響應式程式設計(Reactive Programming),通常將狀態包裝成可觀察物件,於是我們就可以使用可觀察物件的所有能力,一旦狀態物件變更,就能自動獲得更新。

單一store和多store

store是應用管理資料的地方,在Redux應用中,我們總是將所有共享的應用資料集中在一個大的store中,而Mobx則通常按模組將應用狀態劃分,在多個獨立的store中管理。

JavaScript物件和可觀察物件

Redux預設以JavaScript原生物件形式儲存資料,而Mobx使用可觀察物件:

  1. Redux需要手動追蹤所有狀態物件的變更;
  2. Mobx中可以監聽可觀察物件,當其變更時將自動觸發監聽;

不可變(Immutable)和可變(Mutable)

Redux狀態物件通常是不可變的(Immutable):

switch (action.type) {
  case REQUEST_POST:
  	return Object.assign({}, state, {
      post: action.payload.post
  	});
  default:
    retur nstate;
}
複製程式碼

我們不能直接操作狀態物件,而總是在原來狀態物件基礎上返回一個新的狀態物件,這樣就能很方便的返回應用上一狀態;而Mobx中可以直接使用新值更新狀態物件。

mobx-react和react-redux

使用Redux和React應用連線時,需要使用react-redux提供的Providerconnect

  1. Provider:負責將Store注入React應用;
  2. connect:負責將store state注入容器元件,並選擇特定狀態作為容器元件props傳遞;

對於Mobx而言,同樣需要兩個步驟:

  1. Provider:使用mobx-react提供的Provider將所有stores注入應用;
  2. 使用inject將特定store注入某元件,store可以傳遞狀態或action;然後使用observer保證元件能響應store中的可觀察物件(observable)變更,即store更新,元件檢視響應式更新。

選擇Mobx的原因

  1. 學習成本少:Mobx基礎知識很簡單,學習了半小時官方文件和示例程式碼就搭建了新專案例項;而Redux確較繁瑣,流程較多,需要配置,建立store,編寫reducer,action,如果涉及非同步任務,還需要引入redux-thunkredux-saga編寫額外程式碼,Mobx流程相比就簡單很多,並且不需要額外非同步處理庫;
  2. 物件導向程式設計:Mobx支援物件導向程式設計,我們可以使用@observable and @observer,以物件導向程式設計方式使得JavaScript物件具有響應式能力;而Redux最推薦遵循函數語言程式設計,當然Mobx也支援函數語言程式設計;
  3. 模版程式碼少:相對於Redux的各種模版程式碼,如,actionCreater,reducer,saga/thunk等,Mobx則不需要編寫這類别範本程式碼;

不選擇Mobx的可能原因

  1. 過於自由:Mobx提供的約定及模版程式碼很少,這導致開發程式碼編寫很自由,如果不做一些約定,比較容易導致團隊程式碼風格不統一,所以當團隊成員較多時,確實需要新增一些約定;
  2. 可擴充,可維護性:也許你會擔心Mobx能不能適應後期專案發展壯大呢?確實Mobx更適合用在中小型專案中,但這並不表示其不能支撐大型專案,關鍵在於大型專案通常需要特別注意可擴充性,可維護性,相比而言,規範的Redux更有優勢,而Mobx更自由,需要我們自己制定一些規則來確保專案後期擴充,維護難易程度;

程式碼對比

接下來我們使用Redux和Mobx簡單實現同一應用,對比其程式碼,看看它們各自有什麼表現。

架構

在Redux應用中,我們首先需要配置,建立store,並使用redux-thunkredux-saga中介軟體以支援非同步action,然後使用Provider將store注入應用:

// src/store.js
import { applyMiddleware, createStore } from "redux";
import createSagaMiddleware from 'redux-saga'
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from "./reducers";
import App from './containers/App/';

const sagaMiddleware = createSagaMiddleware()
const middleware = composeWithDevTools(applyMiddleware(sagaMiddleware));

export default createStore(rootReducer, middleware);

// src/index.js
…
ReactDOM.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>,
  document.getElementById('app')
);
複製程式碼

Mobx應用則可以直接將所有store注入應用:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'mobx-react';
import { BrowserRouter } from 'react-router-dom';
import { useStrict } from 'mobx';
import App from './containers/App/';
import * as stores from './flux/index';

// set strict mode for mobx
// must change store through action
useStrict(true);

render(
  <Provider {...stores}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('app')
);
複製程式碼

注入Props

Redux:

// src/containers/Company.jsclass CompanyContainer extends Component {
  componentDidMount () {
    this.props.loadData({});
  }
  render () {
    return <Company
      infos={this.props.infos}
      loading={this.props.loading}
    />
  }
}
…

// function for injecting state into props
const mapStateToProps = (state) => {
  return {
    infos: state.companyStore.infos,
    loading: state.companyStore.loading
  }
}

const mapDispatchToProps = dispatch => {
  return bindActionCreators({
      loadData: loadData
  }, dispatch);
}

// injecting both state and actions into props
export default connect(mapStateToProps, { loadData })(CompanyContainer);
複製程式碼

Mobx:

@inject('companyStore')
@observer
class CompanyContainer extends Component {
  componentDidMount () {
    this.props.companyStore.loadData({});
  }
  render () {
    const { infos, loading } = this.props.companyStore;
    return <Company
      infos={infos}
      loading={loading}
    />
  }
}
複製程式碼

定義Action/Reducer等

Redux:

// src/flux/Company/action.jsexport function fetchContacts(){
  return dispatch => {
    dispatch({
      type: 'FREQUEST_COMPANY_INFO',
      payload: {}
    })
  }
}
…

// src/flux/Company/reducer.js
const initialState = {};
function reducer (state = initialState, action) {
  switch (action.type) {
    case 'FREQUEST_COMPANY_INFO': {
      return {
        ...state,
        contacts: action.payload.data.data || action.payload.data,
        loading: false
      }
    }
    default:
      return state;
  }
}
複製程式碼

Mobx:

// src/flux/Company/store.js
import { observable, action } from 'mobx';

class CompanyStore {
  constructor () {
    @observable infos = observable.map(infosModel);
  }

  @action
  loadData = async(params) => {
    this.loading = true;
    this.errors = {};
    return this.$fetchBasicInfo(params).then(action(({ data }) => {
      let basicInfo = data.data;
      const preCompanyInfo = this.infos.get('companyInfo');
      this.infos.set('companyInfo', Object.assign(preCompanyInfo, basicInfo));
      return basicInfo;
    }));
  }

  $fetchBasicInfo (params) {
    return fetch({
      ...API.getBasicInfo,
      data: params
    });
  }
}
export default new CompanyStore();
複製程式碼

非同步Action

如果使用Redux,我們需要另外新增redux-thunkredux-saga以支援非同步action,這就需要另外新增配置並編寫模板程式碼,而Mobx並不需要額外配置。

redux-saga主要程式碼有:

// src/flux/Company/saga.js
// Sagas
// ------------------------------------
const $fetchBasicInfo = (params) => {
  return fetch({
    ...API.getBasicInfo,
    data: params
  });
}

export function *fetchCompanyInfoSaga (type, body) {
  while (true) {
    const { payload } = yield take(REQUEST_COMPANY_INFO)
    console.log('payload:', payload)
    const data = yield call($fetchBasicInfo, payload)
    yield put(receiveCompanyInfo(data))
  }
}
export const sagas = [
  fetchCompanyInfoSaga
];
複製程式碼

一些想法

無論前端還是後端,遇到問題,大多數時候也許大家總是習慣於推薦已經普遍推廣使用的,習慣,熟悉就很容易變成順理成章的,我們應該更進一步去思考,合適的才是更好的。

當然對於“Redux更規範,更靠譜,應該使用Redux”或"Redux模版太多,太複雜了,應該選擇Mobx"這類推斷,我們也應該避免,這些都是相對而言,每個框架都有各自的實現,特色,及其適用場景,正比如Redux流程更復雜,但熟悉流程後就更能把握它的一些基礎/核心理念,使用起來可能更有心得及感悟;而Mobx簡單化,把大部分東西隱藏起來,如果不去特別研究就不能接觸到它的核心/基本思想,也許使得開發者一直停留在使用層次。

所以無論是技術棧還是框架。類庫,並沒有絕對的比較我們就應該選擇什麼,拋棄什麼,我們應該更關注它們解決什麼問題,它們解決問題的關注點,或者說實現方式是什麼,它們的優缺點還有什麼,哪一個更適合當前專案,以及專案未來發展。

參考

  1. An in-depth explanation of Mobx
  2. Redux & Mobx
  3. Mobx
  4. Redux vs Mobx

相關文章