React組件設計實踐總結05 - 狀態管理

今天是 520,這是本系列最後一篇文章,主要涵蓋 React 狀態管理的相關方案。html

前幾篇文章在掘金首發基本石沉大海, 沒什麼閱讀量. 多是文章篇幅太長了?掘金值過低了? 仍是錯別字太多了? 後面靜下心來想一想,寫做對我來講是一種學習和積累的過程, 讓我學習更全面更系統性去描述一個事物. 可是寫做確實是一件很是耗時的事情, 文章的每句話都要細細推敲, 還要避免主觀性太強避免誤導了別人.前端

因此模仿<<內核恐慌>>的口號: "想看的人看,不想看的人就別看"vue


系列目錄node


文章目錄react




狀態管理

如今的前端框架,包括 React 的一個核心思想就是數據驅動視圖, 即UI = f(state). 這種開發方式的變化其實得益於 Virtual-DOM, 它使得咱們不須要關心瀏覽器底層 DOM 的操做細節,只需關心‘狀態(state)’和‘狀態到 UI 的映射關係(f)’. 因此若是你是初學者,不能理解什麼是‘數據驅動’, 仍是不推薦繼續閱讀文章下面的內容git

可是隨着 state 的複雜化, 框架現有的組件化方式很難駕馭 f(視圖的映射關係變得複雜, 難以被表達和維護); 或者相關類型的應用數據流原本就比較複雜, 組件之間的交互關係多樣,原本難以使用UI = f(state)這種關係來表達; 或者應用的組件狀態過於離散,須要統一的治理等等. 咱們就有了狀態管理的需求.github

狀態管理最基礎的解決方式就是分層,也就是說和傳統的 MV* 模式沒有本質區別, 主流狀態管理的主要結構基本都是這樣的:web

他們基本都包含這些特色:typescript

  • 分離視圖和狀態. 狀態管理器擅長狀態管理,因此他們通常會將應用狀態聚合在一塊兒管理,而視圖退化爲貧血視圖(只關注展現),這樣就能夠簡化f映射關係, 讓UI = f(state)這個表達式更完全
  • 約束狀態的變動。Redux 要求經過dispatch+reducer, mobx 要求數據變動函數使用action裝飾或放在flow函數中,目的就是讓狀態的變動根據可預測性
  • 單向數據流。數據流老是按照 Store -> View -> Store 這樣的方式流動, 簡化數據流

可是, React 的狀態管理方案太多了,選擇這些方案可能會讓人抓狂,你須要權衡不少東西:shell

  • 面向對象仍是函數式仍是函數響應式?
  • 單 Store 仍是多 Store?
  • 不可變數據仍是可變數據?
  • 寫代碼爽仍是後期維護爽?
  • 自由仍是約束?
  • 強類型仍是弱類型?
  • 範式化數據仍是非範式化?
  • React 原生仍是第三方?
  • ...



你不須要狀態管理

對於大部分簡單的應用和中後臺項目來講是不須要狀態管理的。說實話這些應用和傳統 web 頁面沒什麼區別, 每一個頁面都各自獨立,每次打開一個新頁面時拉取最新數據,增刪改查僅此而已. 對於這些場景 React 的組件狀態就能夠知足, 沒有必要爲了狀態管理而狀態管理. 這種各自獨立的‘靜態’頁面,引入狀態管理就是過分設計了。

在考慮引入狀態管理以前考慮一下這些手段是否能夠解決你的問題:

  • 是否能夠經過擡升 State 來實現組件間通訊?
  • 若是跨越的層級太多,數據是否能夠經過 Context API 來實現共享?
  • 一些全局狀態是否能夠放在 localStorage 或 sessionStorage 中?
  • 數據是否能夠經過外置的事件訂閱器進行共享?
  • ...



你不須要複雜的狀態管理

當你的應用有如下場景時,就要開始考慮狀態管理:

  • 組件之間須要狀態共享。同一份數據須要響應到多個視圖,且被多個視圖進行變動
  • 須要維護全局狀態,並在他們變更時響應到視圖
  • 數據流變得複雜,React 組件自己已經沒法駕馭。例如跨頁面的用戶協做
  • 須要統一管理應用的狀態。好比實現持久化,可恢復,可撤銷/重作
  • ...

首先肯定是否須要 Redux、Mobx 這些複雜的狀態管理工具? 在 2019 他們不少功能均可以被 React 自己提供的特性取代. 隨着 React 16.3 發佈了新的 Context API,咱們能夠方便地在它之上作簡單的狀態管理, 咱們應該優先選擇這些原生態的狀態管理方式

例如: 簡單的使用 Context API 來作狀態管理:

最近 hooks 用得比較爽(參考上一篇文章: 組件的思惟),我就想配合 Context API 作一個狀態管理器, 後來發現早就有人這麼幹了: unstated-next, 代碼只有 38 行(Hooks+Context),接口很是簡單:

依賴於 hooks 自己靈活的特性,咱們能夠用它來作不少東西, 僅限於想象力. 例如異步數據獲取:

抑或者實現 Redux 的核心功能:

Edit redux hooks


總結一下使用 hooks 做爲狀態管理器的優勢:

  • 極簡。如上
  • 可組合性. hooks 只是普通函數, 能夠組合其餘 hooks,以及其餘Hooks Container. 上一篇文章提到 hooks 寫着寫着很像組件,組件寫着寫着很像 hooks,在用法上組件能夠認爲是一種'特殊'的 hooks。相比組件, hooks 有更靈活的組合特性
  • 以 react 之名. 基於 Context 實現組件狀態共享,基於 hooks 實現狀態管理, 這個方式足夠通用.
  • hooks 不少靈活的特性足以取代相似 Mobx 這些框架的大部分功能
  • 只是普通的 React 組件,能夠在 React inspector 上調試
  • 強類型
  • 基於Context API更容易實現模塊化(或者分形)

須要注意的地方

  • 沒有外置的狀態. 狀態在組件內部,沒有方法從外部觸發狀態變動
  • 缺乏約束. 是好處也是壞處, 對於團隊和初學者來講沒有約束會致使風格不統一,沒法控制項目熵增。好處是能夠自定義本身的約束
  • 性能優化. 須要考慮 Context 變動帶來的性能問題
  • 調試體驗不如 Redux
  • 沒有數據鏡像, 不能實現諸如事件管理的需求
  • 沒有 Redux 豐富的生態

因此 Context+ Hooks 能夠用於知足簡單的狀態管理需求, 對於複雜的狀態管理需求仍是須要用上 Redux、Mobx 這類專業的狀態管理器.


其餘相似的方案

擴展



Redux

unstated 是一個極簡的狀態管理方案,其做者也說了不要認爲unstated 是一個 Redux killer, 不要在它之上構建複雜的工具,也就是不要重複造輪子。因此通常到了這個地步, 其實你就應該考慮 Redux、Mobx、Rxjs 這些複雜的狀態管理框架了。

Redux 是學習 React 繞不過的一個框架. 儘管 Redux 的代碼只有一百多行,概念卻不少,學習曲線很是陡峭,看官方文檔就知道了。即便它的實現很簡潔,可是開發代碼並不簡潔(和 mobx 相反, 髒活留給開發者),尤爲遵循它的'最佳實踐',是從頭開始構建一個項目是很是繁瑣的. 還在如今有相似 dva 或 rematch 這樣的二次封裝庫來簡化它.

本文不打算深刻介紹 Redux 的相關實踐, 社區上面有很是多的教程,官方文檔也很是詳盡. 這裏會介紹 Redux 的主要架構和核心思想,以及它的適用場景.

Redux 的主要結構如上,在此以前你先要搞清楚 Redux 的初衷是什麼,才能明白它爲何要這麼設計. 在我看來 Redux 主要爲了解決如下兩個問題:

  1. 可預測狀態
  2. 簡化應用數據流

其實這也是 flux 的初衷, 只是有些它有些東西沒作好. 明白 Redux 的初衷,如今來看看它的設計就會清晰不少

  • 單一數據源 -> 可預測,簡化數據流:數據只能在一個地方被修改

    • 能夠簡化應用數據流. 解決傳統多 model 模型數據流混亂問題(好比一個 model 能夠修改其餘 model,一個 view 受到多個 model 驅動),讓數據變更變得可預測可調試

    • 同構化應用開發

    • 方便調試

    • 方便作數據鏡像. 能夠實現撤銷/重作、時間旅行、熱重載、狀態持久化和恢復

  • 單向數據流 -> 簡化數據流, 可預測

  • 不能直接修改狀態 -> 可預測

    • 只能經過 dispatch action 來觸發狀態變動. action 只是一個簡單的對象, 攜帶事件的類型和 payload
    • reducer 接收 action 和舊的 state, 規約生成新的 state. reducer 只是一個純函數,能夠嵌套組合子 reducer 對複雜 state 樹進行規約
    • 不可變數據.
    • 可測試.
  • 範式化和反範式化. Store 只存儲範式化的數據,減小數據冗餘。視圖須要的數據經過 reselect 等手段反範式化

  • 經過中間件隔離反作用 -> 可預測 能夠說 Redux 的核心概念就是 reducer,然而這是一個純函數。爲了實現複雜的反作用,redux 提供了相似 Koa 的中間件機制,實現各類反作用. 好比異步請求. 除此以外,能夠利用中間件機制,實現通用的業務模式, 減小代碼重複。

  • Devtool -> 可預測。經過開發者工具能夠可視化數據流


何時應該使用 Redux?

首先仍是警告一下: You Might Not Need Redux, Redux 不是你的第一選擇。

當咱們須要處理複雜的應用狀態,且 React 自己沒法知足你時. 好比:

  • 你須要持久化應用狀態, 這樣你能夠從本地存儲或服務器返回數據中恢復應用
  • 須要實現撤銷重作這些功能
  • 實現跨頁面的用戶協做
  • 應用狀態很複雜時
  • 數據流比較複雜時
  • 許多不相關的組件須要共享和更新狀態
  • 外置狀態
  • ...

最佳實踐

我的以爲react-boilerplate是最符合官方‘最佳實踐’的項目模板. 它的應用工做流以下:

特性:

  1. 整合了 Redux 生態比較流行的方案: immer(不可變數據變動),redux-saga(異步數據流處理),reselect(選取和映射 state,支持 memo,可複合),connected-react-router(綁定 react-router v4)
  2. 根據頁面分割 saga 和 reducer。見下面 👇 和目錄結構
  3. 按需加載 saga 和 reducer(經過 replaceReducer)
  4. 劃分容器組件和展現組件

再看看 react-boilerplate 目錄結構. 這是我我的比較喜歡的項目組件方式,組織很是清晰,頗有參考意義

/src
  /components        # 展現組件
  /containers        # 🔴容器/頁面組件
    /App             # 根組件, 例如放置Provider和Router
    /HomePage        # 頁面組件
      index.js       # 頁面入口
      constants.js   # 🔴 在這裏定義各類常量。包括Action Type
      actions.js     # 🔴 定義各類Action函數
      saga.js        # 🔴 redux-saga 定義各類saga方法, 用於處理異步流程
      reducer.js     # 🔴 reducer。 頁面組件的reducer和saga都會按需注入到根store
      selectors.js   # 🔴 redux state映射和計算
      message.js
      Form.js        # 各類局部組件
      Input.js
      ...
    /FeaturePage     # 其餘頁面組件結構同上
    ...
  /translations      # i18n 翻譯文件
  /utils
    reducerInjectors.js  # 🔴reducer 注入器, 實現和頁面組件一塊兒按需注入
    sagaInjectors.js     # 🔴saga 注入器, 同上
    lodable.js
  app.js             # 應用入口
  i18n.js            # i18n配置
  configureStore.js  # 🔴 建立和配置Redux Store
  reducers.js        # 🔴 根reducers, 合併全部'頁面狀態'和'全局狀態'(如router, language, global(例如用戶鑑權信息))
複製代碼

🤬 開始吐槽!


  • 一,Redux 核心庫很小,只提供了 dispatch 和 reducer 機制,對於各類複雜的反作用處理 Redux 經過提供中間件機制外包出去。社區有不少解決方案,redux-promise, redux-saga, redux-observable... 查看 Redux 的生態系統.

    Redux 中間件的好處是擴展性很是好, 開發者能夠利用中間件抽象重複的業務 e 中間件生態也百花齊放, 可是對於初學者則不友好.

    TM 起碼還得須要去了解各類各樣的庫,橫向比較的一下才知道本身須要搭配哪一個庫吧? 那好吧,就選 redux-saga 吧,star 數比較多。後面又有牛人說不要面向 star 編程,選擇適合本身團隊的纔是最好的... 因此挑選合適的方案以前仍是得要了解各類方案自己吧?。

    Vue 之因此學習曲線比較平緩也在於此吧。它幫助咱們作了不少選擇,提供簡潔的解決方案,另外官方還提供了風格指南和最佳實踐. 這些選擇適合 80%以上的開發需求. 開發者減小了不少折騰的時間,能夠專心寫業務. 這纔是所謂的‘漸進式’框架吧, 對於不愛折騰的或初學者,咱們幫你選擇,但也不會阻礙你往高級的地方走。 這裏能夠感覺到 React 社區和 Vue 社區的風格徹底不一樣.

    在出現選擇困難症時,仍是看看別人怎麼選擇,好比比較有影響力的團隊或者流行的開源項目(如 dva,rematch),選取一個折中方案, 後續有再慢慢深刻研究. 對於 Redux 目前比較流行的組合就是: immer+saga+reselect


  • 二,太多模板代碼。好比上面的 react-boilerplate, 涉及五個文件, 須要定義各類 Action Type、Action、 Reducer、Saga、Select. 因此即使想進行一個小的狀態變化也須要更改好幾個地方:

    筆者我的更喜歡相似 Vuex 這種Ducks風格的組織方式,將模塊下的 action,saga,reducer 和 mapper 都組織在一個文件下面:

    Redux 的二次封裝框架基本採用相似的風格, 如rematch

    這些二次封裝框架通常作了如下優化(其實能夠當作是 Vuex 的優勢),來提高 Redux 的開發體驗:

    • 使用 Ducks 風格組織代碼.聚合分散的 reducer,saga,actions...
    • 更簡化的 API
    • 提供了簡單易用的模塊化(或者稱爲‘分形’)或者命名空間機制。模塊自己支持‘狀態隔離’,讓模塊的 reducer、saga 只專一於模塊本身的狀態. 另外模塊還考慮動態加載
    • 內置反作用處理機制。如使用 saga 或 redux-promise
    • 簡化了不可變數據的操做方式。 如使用 immer
    • 簡化 reducer。Redux 內置了 combineReducers 來複合多個 reducer,在 reducer 內部咱們通常使用 switch 語句來接收 action 和處理數據變更, 其實寫起來很是囉嗦. Vuex 和這些封裝框架不約而同使用了 key/value 形式, 更爲簡潔明瞭
    • 簡化 view 層的 connect 接口。如簡化 mapProps,mapDispatch 這些代碼寫起來也比較繁瑣

  • 三,強制不可變數據。前面文章也提到過 setState 很囉嗦,爲了保證狀態的不可變性最簡單的方式是使用對象展開或者數組展開操做符, 再複雜點能夠上 Immutable.js, 這須要一點學習成本. 好在如今有 immer,能夠按照 Javascript 的對象操做習慣來實現不可變數據


  • 四,狀態設計

    數據類型通常分爲領域數據(Domain data)應用數據(或者稱爲 UI 數據). 在使用 Redux 時常常須要考慮狀態要放在組件局部,仍是全部狀態都抽取到 Redux Store?把這些數據放到 Redux Store 裏面處理起來好像更麻煩?既然都使用 Redux 了,不把數據抽取到 Redux Store 是否不符合最佳實踐? 筆者也時常有這樣的困惑, 你也是最佳實踐的受害者?

    我以爲能夠從下面幾個點進行考慮:

    • 領域數據仍是應用數據? 領域數據通常推薦放在 ReduxStore 中,咱們一般會將 Redux 的 Store 看做一個數據庫,存放範式化的數據。
    • 狀態是否會被多個組件或者跨頁面共享? Redux Store 是一個全局狀態存儲器,既然使用 Redux 了,有理由讓 Redux 來管理跨越多組件的狀態
    • 狀態是否須要被鏡像化? 若是你的應用要作‘時間旅行(撤銷/重作)’或者應用持久化,這個狀態須要被恢復,那麼應該放到 Redux Store,集中化管理數據是 Redux 的強項
    • 狀態是否須要跨越組件的生命週期? 將狀態放在組件局部,就會跟着組件一塊兒被銷燬。若是但願狀態跨越組件的生命週期,應該放到父組件或者 Redux Store 中. 好比一個模態框編輯的數據在關閉後是否須要保留

    原則是能放在局部的就放在局部. 在局部狀態和全局狀態中取捨須要一點開發經驗.

    另外做爲一個集中化的狀態管理器,爲了狀態的可讀性(更容易理解)和可操做性(更容易增刪查改),在狀態結構上面的設計也須要花費一些精力的. 這個數據庫結構的設計方法是同樣的, 在設計狀態以前你須要理清各類領域對象之間的關係, 在數據獲取和數據變動操做複雜度/性能之間取得平衡.

    Redux 官方推薦範式化 State,扁平化結構樹, 減小嵌套,減小數據冗餘. 也就是傾向於更方便被更新和存儲,至於視圖須要什麼則交由 reselect 這些庫進行計算映射和組合.

    因此說 Redux 沒那麼簡單, 固然 80%的 Web 應用也不須要這麼複雜.


  • 五,不方便 Typescript 類型化。無論是 redux 仍是二次封裝框架都不是特別方便 Typescript 進行類型推導,尤爲是在加入各類擴展後。你可能須要顯式註解不少數據類型

    擴展: react-redux-typescript-guide, rematch & Typescript

  • 六,不是分形(Fractal)

    在沒有看到@楊劍鋒的這條知乎回答以前我也不知道什麼叫分形, 我只能嘗試解釋一下我對分形的理解:

    前面文章也提到過‘分離邏輯和視圖’和‘分離容器組件和展現組件’,這兩個規則都來自於 Redux 的最佳實踐。Redux 就是一個'非分形的架構',以下圖,在這種簡單的‘橫向分層'下, 視圖和邏輯(或狀態)能夠被單獨複用,但在 Redux 中卻很難將兩者做爲一個總體的組件來複用:

    集中化的 Store,再經過 Connect 機制可讓狀態在整個應用範圍內被複用;Dumb 組件抽離的狀態和行爲,也容易被複用

    如今假設你須要將單個 container 抽離成獨立的應用,單個 container 是沒法獨立工做的。在分形的架構下,一個‘應用’有更小的‘應用’組成,‘應用’內部有本身的狀態機制,單個應用能夠獨立工做,也能夠做爲子應用. 例如 Redux 的鼻祖 Elm 的架構:

    Store的結構和應用的結構保持一致, 每一個 Elm 組件也是一個 Elm 應用,包含完整的Action、Update、Model和View. 使得單獨的應用能夠被複用

    Redux 不是分形和 Redux 自己的定位有關,它是一個純粹的狀態管理器,不涉及組件的視圖實現,因此沒法像 elm 和 cyclejs 同樣造成一個完整的應用閉環。 其實能夠發現 react 組件自己就是分形的,組件本來就是狀態和視圖的集合.

    分形的好處就是能夠實現更靈活的複用和組合,減小膠水代碼。顯然如今支持純分形架構的框架並不流行,緣由多是門檻比較高。我的認爲不支持分形在工程上還不至於成爲 Redux 的痛點,咱們能夠經過‘模塊化’將 Redux 拆分爲多個模塊,在多個 Container 中進行獨立維護,從某種程度上是否就是分形?另外這種橫向隔離的 UI 和狀態,也是有好處的,好比 UI 相比業務的狀態變化的頻度會更大.

    我的感受到頁面這個級別的分化剛恰好,好比方便分工。好比最近筆者就有這樣一個項目, 咱們須要將一個原生 Windows 客戶端轉換成 electron 實現,限於資源問題,這個項目涉及到兩個團隊之間協做. 對於這個項目應用 Store 就是一個接口層,Windows 團隊負責在這裏維護狀態和實現業務邏輯,而咱們前端團隊則負責展現層. 這樣一來 Windows 不須要學習 React 和視圖展現,咱們也不須要關係他們複雜的業務邏輯(底層仍是使用 C++, 暴露部分接口給 node)


七,可能還有性能問題


總結

本節主要介紹的 Redux 設計的動機,以及圍繞着這個動機一系列設計, 再介紹了 Redux 的一些缺點和最佳實踐。Redux 的生態很是繁榮,若是是初學者或不想折騰仍是建議使用 Dva 或 rematch 這類二次封裝框架,這些框架一般就是 Redux 一些最佳實踐的沉澱, 減小折騰的時間。固然這只是個開始,組織一個大型項目你還有不少要學的。


擴展閱讀




Mobx

Mobx 提供了一個相似 Vue 的響應式系統,相對 Redux 來講 Mobx 的架構更容易理解。 拿官方的圖來看:

  • 響應式數據. 首先使用@observable 將數據轉換爲‘響應式數據’,相似於 Vue 的 data。這些數據在一些上下文(例如 computed,observer 的包裝的 React 組件,reaction)中被訪問時能夠被收集依賴,當這些數據變更時相關的依賴就會被通知.

    響應式數據帶來的兩個優勢是 ① 簡化數據操做方式(相比 redux 和 setState); ② 精確的數據綁定,只有數據真正變更時,視圖才須要渲染,組件依賴的粒度越小,視圖就能夠更精細地更新

  • 衍生.

    • 衍生數據。Mobx 也推薦不要在狀態中放置冗餘或可推導的數據,而是使用 @computed 計算衍生的狀態. computed 的概念相似於 Redux 中的 reselect,對範式化的數據進行反範式化或者聚合計算
    • 反作用衍生. 當數據變更時觸發依賴該數據的反作用,其中包含‘視圖’。視圖是響應式數據的映射
  • 數據變動. mobx 推薦在 action/flow(異步操做) 中對數據進行變動,action 能夠認爲是 Redux 中的 dispatch+reducer 的合體。在嚴格模式下 mobx 會限制只能在 action 函數中進行變動,這使得狀態的變動可被追溯。推薦在 flow 函數中隔離反作用,這個東西和 Redux-saga 差很少,經過 generator 來進行異步操做和反作用隔離


上面就是 Mobx 的核心概念。舉一個簡單的例子:

可是Mobx 不是一個框架,它不會像 Redux 同樣告訴你如何去組織代碼,在哪存儲狀態或者如何處理事件, 也沒有最佳實踐。好處是你能夠按照本身的喜愛組件項目,好比按照 Redux(Vuex)方式,也可使用面向對象方式組織; 壞處是若是你沒有相關經驗, 會不知所措,不知道如何組織代碼

Mobx 通常使用面向對象的方式對 Store 進行組織, 官方文檔構建大型可擴展可維護項目的最佳實踐也介紹了這種方式, 這個其實就是經典的 MV* 模式:

src/
  components/          # 展現組件
  models/              # 🔴 放置一些領域對象
    Order.ts
    User.ts
    Product.ts
    ...
  stores/              # store
    AppStore.ts        # 應用Store,存放應用全局信息,如auth,language,theme
    OrderStore.ts
    RootStore.ts       # 根Store,組合全部下級Store
    ...
  containers/
    App/               # 根組件
    Orders/            # 頁面組件
    ...
  utils/
  store.ts             # store初始化
  index.tsx
複製代碼

領域對象

面向對象領域有太多的名詞和概念,並且比較抽象,若是理解有誤請糾正. 暫且不去理論領域對象是什麼,尚且視做是現實世界中一個業務實體在 OOP 的抽象. 具體來講能夠當作MVC模式中的 M, 或者是 ORM 中數據庫中映射出來的對象.

對於複雜的領域對象,會抽取爲單獨的類,好比前面例子中的Todo類, 抽取爲類的好處是它具備封裝性,能夠包含關聯的行爲、定義和其餘對象的關聯關係,相比純對象表達能力更強. 缺點就是很差序列化

由於它們和頁面的關聯關係較弱,且可能在多個頁面中被複用, 因此放在根目錄的models/下. 在代碼層面領域對象有如下特色:

  • 定義了一些字段(@observable)和一些領域對象的操做方法(@action), 可能還關聯其餘領域對象,好比訂單會關聯用戶和產品
  • 由 Store 來管理生命週期,或者說 Store 就 Model 的容器, 至關於數據庫. Store 一般也是單例

示例

import { observable } from 'mobx';

export default class Order {
  public id: string;

  @observable
  public name: string;

  @observable
  public createdDate: Date;

  @observable
  public product: Product;

  @observable
  public user: User;
}
複製代碼

Store

Store 只是一個 Model 容器, 負責管理 model 對象的生命週期、定義衍生狀態、封裝反作用、和後端接口集成等等. Store 通常是單例. 在 Mobx 應用中通常會劃分爲多個 Store 綁定不一樣的頁面。

示例

import { observable, computed, reaction } from 'mobx';

export default class OrderStore {
    // 定義模型state
  @observable orders: Order[] = [];

  _unSubscribeOrderChange: Function
  rootStore: RootStore

  // 定義衍生數據
  @computed get finishedOrderCount() {}
  @computed get finishedOrders() {}

  // 定義反作用衍生
  subscribeOrderChange() {      this._unSubscribeOrderChange = this.orders.observe((changeData) => {} }

  // 定義action
  @action  addOrder (order) {}
  @action  removeOrder (order) {}

  // 或者一些異步的action
  async fetchOrders () {
    const orders = await fetchOrders()
    orders.forEach(item => this.addOrder(new OrderModel(this, item)))
  }

  // 初始化,初始化數據結構,初始化訂閱等等
  initialize () {
    this.subscribeOrderChange()
  }

  // 一些清理工做
  release () {
    this._unSubscribeOrderChange()
  }

  constructor(store: RootStore) {
    // 和rootStore進行通訊
    this.rootStore = store
  }
}
複製代碼

根 Store

class RootStore {
  constructor() {
    this.appStore = new AppStore(this);
    this.orderStore = new OrderStore(this);
    ...
  }
}
複製代碼
<Provider rootStore={new RootStore()}>
  <App />
</Provider>
複製代碼

看一個 真實世界的例子

這種傳統 MVC 的組織方式主要有如下優勢:

  • 好理解, 容易入手. 經典的 MVC 模式、面向對象,咱們再熟悉不過了. 尤爲是熟悉 Java 這些傳統面向對象編程範式的後端開發人員. 上文提到的跨團隊的項目,咱們選擇的就是 mobx 做爲狀態管理器,對於他們來講這是最好理解的方式. 可是對於領域對象和領域 Store 的拆分和設計須要一點經驗
  • 強類型
  • 代碼簡潔。相對 Redux 多餘的模板代碼而言
  • 數據封裝性。使用類表達的數據結構能夠封裝相應的行爲

問題

  • 在多個 Store 之間共享數據比較麻煩. 咱們的作法是讓全部 Store 都繼承一個父類做爲中間者,經過事件訂閱模式在多個 Store 間進行數據通訊
  • 缺少組織。相對 Redux 而言, 狀態過於零散,不加以約束,狀態能夠被隨意修改。咱們不少代碼就是這樣,懶得寫 action,甚至直接在視圖層給狀態賦值. 因此必定要開始嚴格模式
  • 沒有 Magic. 這是一把雙刃劍, Redux 有中間件機制,能夠擴展抽象許多重複的工做, 好比爲異步方法添加 loading 狀態, 可是對 Typescript 不友好; 基於類的方案,無處下手,代碼會比較囉嗦, 但更直觀
  • 無數據快照,沒法實現時間回溯,這是 Redux 的強項,但大部分的應用不須要這個功能; 另外能夠經過 mobx-state-tree 實現
  • 沒法 hot-reload

還有一些 mobx 自己的問題, 這些問題在上一篇文章也提過, 另外能夠看這篇文章(Mvvm 前端數據流框架精講):

  • 組件侵入性. 須要改變 React 組件本來的結構, 例如全部須要響應數據變更的組件都須要使用 observer 裝飾. 組件本地狀態也須要 observable 裝飾, 以及數據操做方式等等. 對 mobx 耦合較深, 往後切換框架或重構的成本很高

  • 兼容性. mobx v5 後使用 Proxy 進行重構, 但 Proxy 在 Chrome49 以後才支持. 若是要兼容舊版瀏覽器則只能使用 v4, v4 有一些, 這些坑對於不瞭解 mobx 的新手很難發現:

    • Observable 數組並不是真正的數組. 好比 antd 的 Table 組件就不認 mobx 的數組, 須要傳入到組件之間使用 slice 進行轉換
    • 向一個已存在的 observable 對象中添加屬性不會被自動捕獲

MV* 只是 Mobx 的其中一種主流組織方式, 不少文章在討論 Redux 和 mobx 時每每會淪爲函數式和麪向對象之爭,而後就下結論說 Redux 更適合大型項目,下這種結論最主要的緣由是 Redux 有更多約束(only one way to do it), 適合項目的演進和團隊協做, 而不在於函數式和麪向對象。固然函數式和麪向對象範式都有本身擅長的領域,例如函數式適合數據處理和複雜數據流抽象,而面向對象適合業務模型的抽象, 因此不要一竿子打死.

換句話說適不適合大型項目是項目組織問題, Mobx 前期並無提出任何解決方案和最佳實踐。這不後來其做者也開發了mobx-state-tree這個神器,做爲 MobX 官方提供的狀態模型構建庫,MST 吸取了 Redux 等工具的優勢,旨在結合不可變數據/函數式(transactionality, traceability and composition)和可變數據/面向對象(discoverability, co-location and encapsulation)二者的優勢, 提供了不少諸如數據鏡像(time travel)、hot reload、action middleware、集成 redux-devtools 以及強類型(Typescript + 運行時檢查(爭議點))等頗有用的特性, 其實它更像是後端 ActiveRecord 這類 ORM 工具, 構建一個對象圖。

典型的代碼:

限於筆者對 MST 實踐很少,並且文章篇幅已經很長,因此就不展開了,後續有機會再分享分享。


仍是得下一個結論, 選擇 Mobx 仍是 Redux? 這裏仍是引用來自MobX vs Redux: Comparing the Opposing Paradigms - React Conf 2017 紀要的結論:

  • 須要快速開發簡單應用的小團隊能夠考慮使用 MobX,由於 MobX 須要開發的代碼量小,學習成本低,上手快,適合於實時系統,儀表盤,文本編輯器,演示軟件,但不適用於基於事件的系統
  • Redux 適用於大團隊開發複雜應用,Redux 在可擴展性和可維護性方面能夠 hold 住多人協做與業務需求多變,適合商業系統、基於事件的系統以及涉及複雜反應的遊戲場景。

上述結論的主要依據是 Redux 對 action / event 做出反應,而 MobX 對 state 變化做出反應。好比當一個數據變動涉及到 Mobx 的多個 Store,能夠體現出 Redux 的方式更加優雅,數據流更加清晰. 前面都詳盡闡述了 Mobx 和 Redux 的優缺點,mobx 還有 MST 加持, 相信讀者內心早已有本身的喜愛


擴展

RxJS

若是上文提到的狀態管理工具都沒法知足你的須要,你的項目複雜程度可能超過全國 99%的項目了. RxJS 可能能夠助你一臂之力, RxJS 很是適合複雜異步事件流的應用,筆者在這方面實踐也比較少,推薦看看徐飛的相關文章, 另外 Redux(Redux-Observable)和 Mobx 實際上也能夠配合 RxJS 使用




其餘狀態管理方案

推薦這篇文章State of React State Management for 2019




擴展閱讀

相關文章
相關標籤/搜索