今天是 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)
這個表達式更完全dispatch+reducer
, mobx 要求數據變動函數使用action
裝飾或放在flow
函數中,目的就是讓狀態的變動根據可預測性可是, React 的狀態管理方案太多了,選擇這些方案可能會讓人抓狂,你須要權衡不少東西:shell
對於大部分簡單的應用和中後臺項目來講是不須要狀態管理的。說實話這些應用和傳統 web 頁面沒什麼區別, 每一個頁面都各自獨立,每次打開一個新頁面時拉取最新數據,增刪改查僅此而已. 對於這些場景 React 的組件狀態就能夠知足, 沒有必要爲了狀態管理而狀態管理. 這種各自獨立的‘靜態’頁面,引入狀態管理就是過分設計了。
在考慮引入狀態管理以前考慮一下這些手段是否能夠解決你的問題:
當你的應用有如下場景時,就要開始考慮狀態管理:
首先肯定是否須要 Redux、Mobx 這些複雜的狀態管理工具? 在 2019 他們不少功能均可以被 React 自己提供的特性取代. 隨着 React 16.3 發佈了新的 Context API,咱們能夠方便地在它之上作簡單的狀態管理, 咱們應該優先選擇這些原生態的狀態管理方式。
例如: 簡單的使用 Context API 來作狀態管理:
最近 hooks 用得比較爽(參考上一篇文章: 組件的思惟),我就想配合 Context API 作一個狀態管理器, 後來發現早就有人這麼幹了: unstated-next, 代碼只有 38 行(Hooks+Context),接口很是簡單:
依賴於 hooks 自己靈活的特性,咱們能夠用它來作不少東西, 僅限於想象力. 例如異步數據獲取:
抑或者實現 Redux 的核心功能:
總結一下使用 hooks 做爲狀態管理器的優勢:
Hooks Container
. 上一篇文章提到 hooks 寫着寫着很像組件,組件寫着寫着很像 hooks,在用法上組件能夠認爲是一種'特殊'的 hooks。相比組件, hooks 有更靈活的組合特性須要注意的地方
因此 Context+ Hooks 能夠用於知足簡單的狀態管理需求, 對於複雜的狀態管理需求仍是須要用上 Redux、Mobx 這類專業的狀態管理器.
其餘相似的方案
擴展
unstated
是一個極簡的狀態管理方案,其做者也說了不要認爲unstated 是一個 Redux killer, 不要在它之上構建複雜的工具,也就是不要重複造輪子。因此通常到了這個地步, 其實你就應該考慮 Redux、Mobx、Rxjs 這些複雜的狀態管理框架了。
Redux 是學習 React 繞不過的一個框架. 儘管 Redux 的代碼只有一百多行,概念卻不少,學習曲線很是陡峭,看官方文檔就知道了。即便它的實現很簡潔,可是開發代碼並不簡潔(和 mobx 相反, 髒活留給開發者),尤爲遵循它的'最佳實踐',是從頭開始構建一個項目是很是繁瑣的. 還在如今有相似 dva 或 rematch 這樣的二次封裝庫來簡化它.
本文不打算深刻介紹 Redux 的相關實踐, 社區上面有很是多的教程,官方文檔也很是詳盡. 這裏會介紹 Redux 的主要架構和核心思想,以及它的適用場景.
Redux 的主要結構如上,在此以前你先要搞清楚 Redux 的初衷是什麼,才能明白它爲何要這麼設計. 在我看來 Redux 主要爲了解決如下兩個問題:
其實這也是 flux
的初衷, 只是有些它有些東西沒作好. 明白 Redux 的初衷,如今來看看它的設計就會清晰不少
單一數據源 -> 可預測,簡化數據流:數據只能在一個地方被修改
能夠簡化應用數據流. 解決傳統多 model 模型數據流混亂問題(好比一個 model 能夠修改其餘 model,一個 view 受到多個 model 驅動),讓數據變更變得可預測可調試
同構化應用開發
方便調試
方便作數據鏡像. 能夠實現撤銷/重作、時間旅行、熱重載、狀態持久化和恢復
單向數據流 -> 簡化數據流, 可預測
不能直接修改狀態 -> 可預測
範式化和反範式化. Store 只存儲範式化的數據,減小數據冗餘。視圖須要的數據經過 reselect 等手段反範式化
經過中間件隔離反作用 -> 可預測 能夠說 Redux 的核心概念就是 reducer,然而這是一個純函數。爲了實現複雜的反作用,redux 提供了相似 Koa 的中間件機制,實現各類反作用. 好比異步請求. 除此以外,能夠利用中間件機制,實現通用的業務模式, 減小代碼重複。
Devtool -> 可預測。經過開發者工具能夠可視化數據流
何時應該使用 Redux?
首先仍是警告一下: You Might Not Need Redux, Redux 不是你的第一選擇。
當咱們須要處理複雜的應用狀態,且 React 自己沒法知足你時. 好比:
最佳實踐
我的以爲react-boilerplate是最符合官方‘最佳實踐’的項目模板. 它的應用工做流以下:
特性:
immer
(不可變數據變動),redux-saga
(異步數據流處理),reselect
(選取和映射 state,支持 memo,可複合),connected-react-router
(綁定 react-router v4)再看看 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 的開發體驗:
三,強制不可變數據。前面文章也提到過 setState 很囉嗦,爲了保證狀態的不可變性最簡單的方式是使用對象展開或者數組展開操做符, 再複雜點能夠上 Immutable.js, 這須要一點學習成本. 好在如今有 immer,能夠按照 Javascript 的對象操做習慣來實現不可變數據
四,狀態設計。
數據類型通常分爲領域數據(Domain data)和應用數據(或者稱爲 UI 數據). 在使用 Redux 時常常須要考慮狀態要放在組件局部,仍是全部狀態都抽取到 Redux Store?把這些數據放到 Redux Store 裏面處理起來好像更麻煩?既然都使用 Redux 了,不把數據抽取到 Redux Store 是否不符合最佳實踐? 筆者也時常有這樣的困惑, 你也是最佳實踐的受害者?
我以爲能夠從下面幾個點進行考慮:
原則是能放在局部的就放在局部. 在局部狀態和全局狀態中取捨須要一點開發經驗.
另外做爲一個集中化的狀態管理器,爲了狀態的可讀性(更容易理解)和可操做性(更容易增刪查改),在狀態結構上面的設計也須要花費一些精力的. 這個數據庫結構的設計方法是同樣的, 在設計狀態以前你須要理清各類領域對象之間的關係, 在數據獲取和數據變動操做複雜度/性能之間取得平衡.
Redux 官方推薦範式化 State,扁平化結構樹, 減小嵌套,減小數據冗餘. 也就是傾向於更方便被更新和存儲,至於視圖須要什麼則交由 reselect 這些庫進行計算映射和組合.
因此說 Redux 沒那麼簡單, 固然 80%的 Web 應用也不須要這麼複雜.
五,不方便 Typescript 類型化。無論是 redux 仍是二次封裝框架都不是特別方便 Typescript 進行類型推導,尤爲是在加入各類擴展後。你可能須要顯式註解不少數據類型
六,不是分形(Fractal)
在沒有看到@楊劍鋒的這條知乎回答以前我也不知道什麼叫分形, 我只能嘗試解釋一下我對分形的理解:
前面文章也提到過‘分離邏輯和視圖’和‘分離容器組件和展現組件’,這兩個規則都來自於 Redux 的最佳實踐。Redux 就是一個'非分形的架構',以下圖,在這種簡單的‘橫向分層'下, 視圖和邏輯(或狀態)能夠被單獨複用,但在 Redux 中卻很難將兩者做爲一個總體的組件來複用:
如今假設你須要將單個 container 抽離成獨立的應用,單個 container 是沒法獨立工做的。在分形的架構下,一個‘應用’有更小的‘應用’組成,‘應用’內部有本身的狀態機制,單個應用能夠獨立工做,也能夠做爲子應用. 例如 Redux 的鼻祖 Elm 的架構:
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 提供了一個相似 Vue 的響應式系統,相對 Redux 來講 Mobx 的架構更容易理解。 拿官方的圖來看:
響應式數據. 首先使用@observable
將數據轉換爲‘響應式數據’,相似於 Vue 的 data。這些數據在一些上下文(例如 computed,observer 的包裝的 React 組件,reaction)中被訪問時能夠被收集依賴,當這些數據變更時相關的依賴就會被通知.
響應式數據帶來的兩個優勢是 ① 簡化數據操做方式(相比 redux 和 setState); ② 精確的數據綁定,只有數據真正變更時,視圖才須要渲染,組件依賴的粒度越小,視圖就能夠更精細地更新
衍生.
@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/
下. 在代碼層面領域對象有如下特色:
示例
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 的組織方式主要有如下優勢:
問題
還有一些 mobx 自己的問題, 這些問題在上一篇文章也提過, 另外能夠看這篇文章(Mvvm 前端數據流框架精講):
組件侵入性. 須要改變 React 組件本來的結構, 例如全部須要響應數據變更的組件都須要使用 observer 裝飾. 組件本地狀態也須要 observable 裝飾, 以及數據操做方式等等. 對 mobx 耦合較深, 往後切換框架或重構的成本很高
兼容性. mobx v5 後使用 Proxy 進行重構, 但 Proxy 在 Chrome49 以後才支持. 若是要兼容舊版瀏覽器則只能使用 v4, v4 有一些坑, 這些坑對於不瞭解 mobx 的新手很難發現:
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 紀要的結論:
上述結論的主要依據是 Redux 對 action / event 做出反應,而 MobX 對 state 變化做出反應。好比當一個數據變動涉及到 Mobx 的多個 Store,能夠體現出 Redux 的方式更加優雅,數據流更加清晰. 前面都詳盡闡述了 Mobx 和 Redux 的優缺點,mobx 還有 MST 加持, 相信讀者內心早已有本身的喜愛
擴展
若是上文提到的狀態管理工具都沒法知足你的須要,你的項目複雜程度可能超過全國 99%的項目了. RxJS 可能能夠助你一臂之力, RxJS 很是適合複雜異步事件流的應用,筆者在這方面實踐也比較少,推薦看看徐飛的相關文章, 另外 Redux(Redux-Observable)和 Mobx 實際上也能夠配合 RxJS 使用
推薦這篇文章State of React State Management for 2019