相信你們在項目開發中,在頁面較複雜的狀況下,每每會遇到一個問題,就是在頁面組件之間通訊會很是困難。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
:app
ΔM = perform(action)
複製代碼
每個 action
都會去改變數據,那麼視圖獲得的數據(state)
就是全部 action
疊加起來的變動,框架
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 應用的新選擇》 有簡單提過,歡迎你們體驗。