相信你們在項目開發中,在頁面較複雜的狀況下,每每會遇到一個問題,就是在頁面組件之間通訊會很是困難。ios
好比說一個商品列表和一個已添加商品列表:git
假如這兩個列表是獨立的兩個組件,它們會共享一個數據 「被選中的商品」,在商品列表
選中一個商品,會影響已添加商品列表
,在已添加列表
中刪除一個商品,一樣會影響商品列表
的選中狀態。github
它們兩個是兄弟組件,在沒有數據流框架的幫助下,在組件內數據有變化的時候,只能經過父組件傳輸數據,每每會有 onSelectedDataChange
這種函數出現,在這種狀況下,還尚且能忍受,若是組件嵌套較深的話,那痛苦能夠想象一下,因此纔有解決數據流的各類框架的出現。redux
咱們知道 React 是 MVC
裏的 V
,而且是數據驅動視圖的,簡單來講,就是數據 => 視圖
,視圖是基於數據的渲染結果:axios
V = f(M)
複製代碼
數據有更新的時候,在進入渲染以前,會先生成 Virtual DOM,先後進行對比,有變化才進行真正的渲染。api
V + ΔV = f(M + ΔM)
複製代碼
數據驅動視圖變化有兩種方式,一種是 setState
,改變頁面的 state
,一種是觸發 props
的變化。bash
咱們知道數據是不會本身改變,那麼確定是有「外力」去推進,每每是遠程請求數據回來或者是 UI
上的交互行爲,咱們統稱這些行爲叫 action
:markdown
ΔM = perform(action)
複製代碼
每個 action
都會去改變數據,那麼視圖獲得的數據(state)
就是全部 action
疊加起來的變動,app
state = actions.reduce(reducer, initState)
複製代碼
因此真實的場景會出現以下或更復雜的狀況:框架
問題就出在,更新數據比較麻煩,混亂,每次要更新數據,都要一層層傳遞,在頁面交互複雜的狀況下,沒法對數據進行管控。
有沒有一種方式,有個集中的地方去管理數據,集中處理數據的接收,修改和分發?答案顯然是有的,數據流框架就是作這個事情,熟悉 Redux
的話,就知道其實上面講的就是 Redux
的核心理念,它和 React
的數據驅動原理是相匹配的。
數據流框架目前佔主要地位的仍是 Redux,它提供一個全局 Store
處理應用數據的接收,修改和分發。
它的原理比較簡單,View
裏面有任何交互行爲須要改變數據,首先要發一個 action
,這個 action
被 Store
接收並交給對應的 reducer
處理,處理完後把更新後的數據傳遞給 View
。Redux
不依賴於任何框架,它只是定義一種方式控制數據的流轉,能夠應用於任何場景。
雖然定義了一套數據流轉的方式,但真正使用上會有很多問題,我我的總結主要是兩個問題:
咱們來看看寫一個數據請求的例子,這是很是典型的案例:
actions.js
export const FETCH_DATA_START = 'FETCH_DATA_START'; export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS'; export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR'; export function fetchData() { return dispatch => { dispatch(fetchDataStart()); axios.get('xxx').then((data) => { dispatch(fetchDataSuccess(data)); }).catch((error) => { dispatch(fetchDataError(error)); }); }; } export function fetchDataStart() { return { type: FETCH_DATA_START, } } ...FETCH_DATA_SUCCESS ...FETCH_DATA_ERROR 複製代碼
reducer.js
import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR } from 'actions.js'; export default (state = { data: null }, action) => { switch (action.type) { case FETCH_DATA_START: ... case FETCH_DATA_SUCCESS: ... case FETCH_DATA_ERROR: ... default: return state } } 複製代碼
view.js
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from 'reducer.js'; import { fetchData } from 'actions.js'; const store = createStore(reducer, applyMiddleware(thunk)); store.dispatch(fetchData()); 複製代碼
第一個問題,發一個請求,由於須要託管請求的全部狀態,因此須要定義不少的 action
,這時很容易會繞暈,就算有人嘗試把這些狀態再封裝抽象,也會充斥着一堆模板代碼。有人會挑戰說,雖然一開始是比較麻煩,繁瑣,但對項目可維護性,擴展性都比較友好,我不太認同這樣的說法,目前還算簡單,真正業務邏輯複雜的狀況下,會顯得更噁心,效率低且閱讀體驗差,相信你們也寫過或看過這樣的代碼,後面本身看回來,須要在 actions
文件搜索一下 action
的名稱,reducer
文件查詢一下,繞一圈才慢慢看懂。
第二個問題,按照官方推薦使用 redux-thunk 實現異步 action
的方法,只要在 action
裏返回一個函數便可,這對有強迫症的人來講,簡直受不了,actions
文件顯得它很不純,原本它只是來定義 action
,卻居然要夾雜着數據請求,甚至 UI
上的交互!
我以爲 Redux
設計上沒有問題,思路很是簡潔,是我很是喜歡的一個庫,它提供的數據的流動方式,目前也是獲得社區的普遍承認。然而在使用上有它的缺陷,雖然是能夠克服,可是它自己難道沒有能夠優化的地方?
dva 的出來就是爲了解決 redux
的開發體驗問題,它首次提出了 model
的概念,很好地把 action
、reducers
、state
結合到一個 model
裏面。
model.js
export default { namespace: 'products', state: [], reducers: { 'delete'(state, { payload: id }) { return state.filter(item => item.id !== id); }, }, }; 複製代碼
它的核心思想就是一個 action
對應一個 reducer
,經過約定,省略了對 action
的定義,默認 reducers
裏面的函數名稱即爲 action
的名稱。
在異步 action
的處理上,定義了 effects(反作用)
的概念,與同步 action
區分起來,內部藉助了 redux-saga 來實現。
model.js
export default { namespace: 'counter', state: [], reducers: { }, effects: { *add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'minus' }); }, }, }; 複製代碼
經過這樣子的封裝,基本保持 Redux
的用法,咱們能夠沉浸式地在 model
編寫咱們的數據邏輯,我以爲已經很好地解決問題了。
不過我我的喜愛問題,不太喜歡使用 redux-saga
這個庫來解決異步流,雖然它的設計很巧妙,利用了 generator
的特性,不侵入 action
,而是經過中間件的方式進行攔截,很好地將異步處理隔離出獨立的一層,而且以此聲稱對實現單元測試是最友好的。是的,我以爲設計上真的很是棒,那時候還特地閱讀了它的源碼,讚歎做者真的牛,這樣的方案都能想出來,可是後來我看到還有更好的解決方案(後面會介紹),就放棄使用它了。
mirrorx 和 dva
差很少,只是它使用了單例的方式,全部的 action
都保存了 actions
對象中,訪問 action
有了另外一種方式。還有就是處理異步 action
的時候可使用 async/await
的方式。
import mirror, { actions } from 'mirrorx' mirror.model({ name: 'app', initialState: 0, reducers: { increment(state) { return state + 1 }, decrement(state) { return state - 1 } }, effects: { async incrementAsync() { await new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 1000) }) actions.app.increment() } } }); 複製代碼
它內部處理異步流的問題,相似 redux-thunk
的處理方式,經過注入一箇中間件,這個中間件裏判斷 當前 action
是否是異步 action
(只要判斷是否是 effects
裏定義的 action
便可),若是是的話,就直接中斷了中間件的鏈式調用,能夠看看這段代碼。
這樣的話,咱們 effects
裏的函數就可使用 async/await
的方式調用異步請求了,其實不是必定要使用 async/await
,函數裏的實現沒有限制,由於中間件只是調用函數執行而已。
我是比較喜歡使用 async/await
這種方式處理異步流,這是我不用 redux-saga
的緣由。
可是我最終沒有選擇使用 mirrorx
或 dva
,由於用它們就捆綁一堆東西,我以爲不該該作成這樣子,爲啥好好的解決 Redux
問題,最後變成都作一個腳手架出來?這不是強制消費嗎?讓人用起來就會有限制。瞭解它們的原理後,我本身參照寫了個 xredux 出來,只是單純解決 Reudx
的問題,不依賴於任何框架,能夠看做只是 Redux
的升級版。
使用上和 mirrorx
差很少,但它和 Redux
是同樣的,不綁定任何框架,能夠獨立使用。
import xredux from "xredux"; const store = xredux.createStore(); const actions = xredux.actions; // This is a model, a pure object with namespace, initialState, reducers, effects. xredux.model({ namespace: "counter", initialState: 0, reducers: { add(state, action) { return state + 1; }, plus(state, action) { return state - 1; }, }, effects: { async addAsync(action, dispatch, getState) { await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); actions.counter.add(); } } }); // Dispatch action with xredux.actions actions.counter.add(); 複製代碼
在異步處理上,其實也存在問題,可能你們也遇到過,就是數據請求有三種狀態的問題,咱們來看看,寫一個數據請求的 effects
:
import xredux from 'xredux'; import { fetchUserInfo } from 'services/api'; const { actions } = xredux; xredux.model({ namespace: 'user', initialState: { getUserInfoStart: false, getUserInfoError: null, userInfo: null, }, reducers: { // fetch start getUserInfoStart (state, action) { return { ...state, getUserInfoStart: true, }; }, // fetch error getUserInfoError (state, action) { return { ...state, getUserInfoStart: false, getUserInfoError: action.payload, }; }, // fetch success setUserInfo (state, action) { return { ...state, userInfo: action.payload, getUserInfoStart: false, }; } }, effects: { async getUserInfo (action, dispatch, getState) { let userInfo = null; actions.user.getUserInfoStart(); try { userInfo = await fetchUserInfo(); actions.user.setUserInfo(userInfo); } catch (e) { actions.user.setUserInfoError(e); } } }, }); 複製代碼
能夠看到,仍是存在不少感受沒用的代碼,一個請求須要3個 reducer
和1個 effect
,當時想着怎麼優化,但沒有很好的辦法,後來我想到這3個 reducer
有個共同點,就是隻是賦值,沒有任何操做,那我內置一個 setState
的 reducer
,專門去處理這種只是賦值的 action
就行了。
最後變成這樣:
import xredux from 'xredux'; import { fetchUserInfo } from 'services/api'; const { actions } = xredux; xredux.model({ namespace: 'user', initialState: { getUserInfoStart: false, getUserInfoError: null, userInfo: null, }, reducers: { }, effects: { async getUserInfo (action, dispatch, getState) { let userInfo = null; // fetch start actions.user.setState({ getUserInfoStart: true, }); try { userInfo = await fetchUserInfo(); // fetch success actions.user.setState({ getUserInfoStart: false, userInfo, }); } catch (e) { // fetch error actions.user.setState({ getUserInfoError: e, }); } } }, }); 複製代碼
這個目前是本身比較滿意的方案,在項目中也有實踐過,寫起來確實比較簡潔易懂,不知你們有沒有更好的辦法。
使用了 Redux
,按道理應用中的狀態數據應該都放到 Store
中,那組件是否能有本身的狀態呢?目前就會有兩種見解:
Store
中託管,全部組件都是純展現組件。Store
託管。這兩種就是分別對應貧血組件和充血組件,區別就是組件是否有本身的邏輯,仍是說只是純展現。我以爲這個問題不用去爭論,沒有對錯。
理論上固然是說貧血組件好,由於這樣保證數據是在一個地方管理的,可是付出的代價多是沉重的,使用了這種方式,每每到後面會有想死的感受,一種想回頭又不想放棄的感受,其實不必這麼執着。
相信你們幾乎都是充血組件,有一些狀態只與組件相關的,由組件去託管,有些狀態須要共享的,交給 Store
去託管,甚至有人全部狀態都有組件託管,也是存在的,由於頁面太簡單,根本就不須要用到數據流框架。
在 React
開發中不可避免會遇到數據流的問題,如何優雅地處理目前也沒有最完美的方案,社區也存在各類各樣的方法,能夠多思考爲何是這樣作,瞭解底層原理比盲目使用別人的方案更重要。
若是想詳細瞭解 xredux 如何在 React
中運用,可使用 RIS 初始化一個 Standard 應用看看,以前的文章《RIS,建立 React 應用的新選擇》 有簡單提過,歡迎你們體驗。