這裏要講的就是一個Redux在React中的應用問題,講一講Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-saga這些包的做用和他們解決的問題。
由於不想把篇幅拉得太長,因此沒有太多源碼分析和語法講解,能怎麼簡單就怎麼簡單。react
先看看百度百科上面Redux的一張圖:ios
這是Redux在Github上的介紹:Redux用於js程序,是一個可預測的狀態容器。git
在這裏咱們首先要明白的是什麼叫可預測?什麼叫狀態容器?github
什麼叫狀態?實際上就是變量,對話框顯示或隱藏的變量,一杯奶茶多少錢的變量。redux
那麼這個狀態容器,實際上就是一個存放這些變量的變量。axios
你建立了一個全局變量叫Store,而後將代碼中控制各個狀態的變量存放在裏面,那麼如今Store就叫作狀態容器。後端
什麼叫可預測?數組
你在操做這個Store的時候,老是用Store.price的方式來設置值,這種操做數據的方式很原始,對於複雜的系統而言永遠都不知道程序在運行的過程當中發生了什麼。promise
那麼如今咱們都經過發送一個Action去作修改,而Store在接收到Action後會使用Reducer對Action傳遞的數據作處理,最後應用到Store中。app
相對於Store.price的方式來修改者,這種方式無疑更麻煩,可是這種方式的好處就是,每個Action裏面均可以寫日誌,能夠記錄各類狀態的變更,這就是可預測。
因此若是你的程序很簡單,你徹底沒有必要去用Redux。
看看Redux的示例代碼:
actionTypes.js:
export const CHANGE_BTN_TEXT = 'CHANGE_BTN_TEXT';
actions.js:
import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
reducers.js:
import * as T from './actionTypes'; const initialState = { btnText: '我是按鈕', }; const pageMainReducer = (state = initialState, action) => { switch (action.type) { case T.CHANGE_BTN_TEXT: return { ...state, btnText: action.payload }; default: return state; } }; export default pageMainReducer;
index.js
import { createStore } from 'redux'; import reducer from './reducers'; import { changeBtnText } from './actions'; const store = createStore(reducer); // 開始監聽,每次state更新,那麼就會打印出當前狀態 const unsubscribe = store.subscribe(() => { console.info(store.getState()); }); // 發送消息 store.dispatch(changeBtnText('點擊了按鈕')); // 中止監聽state的更新 unsubscribe();
這裏就不解釋什麼語法做用了,網上這樣的資料太多了。
Redux是一個可預測的狀態容器,跟React這種構建UI的庫是兩個相互獨立的東西。
Redux要應用到React中,很明顯action,reducer,dispatch這幾個階段並不須要改變,惟一須要考慮的是redux中的狀態須要如何傳遞給react組件。
很簡單,只須要每次要更新數據時運用store.getState獲取到當前狀態,並將這些數據傳遞給組件便可。
那麼問題來了,如何讓每一個組件都獲取到store呢?
固然是將store做爲一個值傳遞給根組件,而後store就會一級一級往下傳,使得每一個組件都能獲取到store的值。
可是這樣太繁瑣了,難道每一個組件須要寫一個傳遞store的邏輯?爲了解決這個問題,那麼得用到React的context玩法,經過在根組件上將store放在根組件的context中,而後在子組件中經過context獲取到store。
react-redux的主要思路也是如此,經過嵌套組件Provider將store放到context中,經過connect這個高階組件,來隱藏取store的操做,這樣咱們就不須要每次去操做context寫一大堆代碼那麼麻煩了。
而後咱們再來基於以前的Redux示例代碼給出react-redux的使用演示代碼,其中action和reduce部分不變,先增長一個組件PageMain:
const PageMain = (props) => { return ( <div> <button onClick={() => { props.changeText('按鈕被點擊了'); }} > {props.btnText} </button> </div> ); }; // 映射store.getState()的數據到PageMain const mapStateToProps = (state) => { return { btnText: state.pageMain.btnText, }; }; // 映射使用了store.dispatch的函數到PageMain const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnText(text)); } }; }; // 這個地方也能夠簡寫,react-redux會自動作處理 const mapDispatchToProps = { changeText: changeBtnText }; export default connect(mapStateToProps, mapDispatchToProps)(PageMain);
注意上面的state.pageMain.btnText,這個pageMain是我用redux的combineReducers將多個reducer合併後給的原先的reducer一個命名。
它的代碼以下:
import { combineReducers } from 'redux'; import pageMain from './components/pageMain/reducers'; const reducer = combineReducers({ pageMain }); export default reducer;
而後修改index.js:
import React from 'react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import ReactDOM from 'react-dom'; import reducer from './reducers'; import PageMain from './components/pageMain'; const store = createStore(reducer); const App = () => ( <Provider store={store}> <PageMain /> </Provider> ); ReactDOM.render(<App />, document.getElementById('app'));
以前咱們講到Redux是個可預測的狀態容器,這個可預測在於對數據的每一次修改均可以進行相應的處理和記錄。
假如如今咱們須要在每次修改數據時,記錄修改的內容,咱們能夠在每個dispatch前面加上一個console.info記錄修改的內容。
可是這樣太繁瑣了,因此咱們能夠直接修改store.dispatch:
let next = store.dispatch store.dispatch = (action)=> { console.info('修改內容爲:', action) next(action) }
Redux中也有一樣的功能,那就是applyMiddleware。直譯過來就是「應用中間件」,它的做用就是改造dispatch函數,跟上面的玩法基本雷同。
來一段演示代碼:
import { createStore, applyMiddleware } from 'redux'; import reducer from './reducers'; const store = createStore(reducer, applyMiddleware(curStore => next => action => { console.info(curStore.getState(), action); return next(action); }));
看起來挺奇怪的玩法,可是理解起來並不難。經過這種返回函數的方法,使得applyMiddleware內部以及咱們使用時能夠處理store和action,而且這裏next的應用就是爲了使用多箇中間件而存在的。
而一般咱們沒有必要本身寫中間件,好比日誌的記錄就已經有了成熟的中間件:redux-logger,這裏給一個簡單的例子:
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; import reducer from './reducers'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
這樣就能夠記錄全部action及其發送先後的state的日誌,咱們能夠了解到代碼實際運行時到底發生了什麼。
在上面的代碼中,咱們點擊按鈕後,直接修改了按鈕的文本,這個文本是個固定的值。
actions.js:
import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
可是在咱們實際生產的過程當中,不少狀況都是須要去請求服務端拿到數據再修改的,這個過程是一個異步的過程。又或者須要setTimeout去作一些事情。
咱們能夠去修改這一部分以下:
const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnText('正在加載中')); axios.get('http://test.com').then(() => { dispatch(changeBtnText('加載完畢')); }).catch(() => { dispatch(changeBtnText('加載有誤')); }); } }; };
實際上,咱們天天不知道要處理多少這樣的代碼。
可是問題來了,異步操做相比同步操做多了一個不少肯定因素,好比咱們展現正在加載中時,可能要先要作異步操做A,而請求後臺的過程卻很是快,致使加載完畢先出現,而這時候操做A才作完,而後再展現加載中。
因此上面的這個玩法並不能知足這種狀況。
這個時候咱們須要去經過store.getState獲取當前狀態,從而判斷究竟是展現正在加載中仍是展現加載完畢。
這個過程就不能放在mapDispatchToProps中了,而須要放在中間件中,由於中間件中能夠拿到store。
首先創造store的時候須要應用react-thunk,也就是
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; const store = createStore( reducer, applyMiddleware(thunk) );
它的源碼超級簡單:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
從這個裏面能夠看出,它就是增強了dispatch的功能,在dispatch一個action以前,去判斷action是不是一個函數,若是是函數,那麼就執行這個函數。
那麼咱們使用起來就很簡單了,此時咱們修改actions.js
import axios from 'axios'; import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; }; export const changeBtnTextAsync = (text) => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加載中')); } axios.get(`http://test.com/${text}`).then(() => { if (getState().isLoading) { dispatch(changeBtnText('加載完畢')); } }).catch(() => { dispatch(changeBtnText('加載有誤')); }); }; };
而原來mapDispatchToProps中的玩法和同步action的玩法是同樣的:
const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnTextAsync(text)); } }; };
經過redux-thunk咱們能夠簡單地進行異步操做,而且能夠獲取到各個異步操做時期狀態的值。
Redux雖然好用,可是裏面仍是有些重複代碼,因此有了redux-actions來簡化那些重複代碼。
這部分簡化工做主要集中在構造action和處理reducers方面。
先來看看原先的actions
import axios from 'axios'; import * as T from './actionTypes'; export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; }; export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加載中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加載完畢')); } }).catch(() => { dispatch(changeBtnText('加載有誤')); }); }; };
而後再來看看修改後的:
import axios from 'axios'; import * as T from './actionTypes'; import { createAction } from 'redux-actions'; export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text); export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加載中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加載完畢')); } }).catch(() => { dispatch(changeBtnText('加載有誤')); }); }; };
這一塊代碼替換上面的部分代碼後,程序運行結果依然保持不變,也就是說createAction只是對上面的代碼進行了簡單的封裝而已。
這裏注意到,異步的action就不要用createAction,由於這個createAction返回的是一個對象,而不是一個函數,就會致使redux-thunk的代碼沒有起到做用。
這裏也可使用createActions這個函數同時建立多個action,可是講道理,這個語法很奇怪,用createAction就好。
一樣redux-actions對reducer的部分也進行了處理,好比handleAction以及handelActions。
先來看看原先的reducers
import * as T from './actionTypes'; const initialState = { btnText: '我是按鈕', }; const pageMainReducer = (state = initialState, action) => { switch (action.type) { case T.CHANGE_BTN_TEXT: return { ...state, btnText: action.payload }; default: return state; } }; export default pageMainReducer;
而後使用handleActions來處理
import { handleActions } from 'redux-actions'; import * as T from './actionTypes'; const initialState = { btnText: '我是按鈕', }; const pageMainReducer = handleActions({ [T.CHANGE_BTN_TEXT]: { next(state, action) { return { ...state, btnText: action.payload, }; }, throw(state) { return state; }, }, }, initialState); export default pageMainReducer;
這裏handleActions能夠加入異常處理,而且幫助處理了初始值。
注意,不管是createAction仍是handleAction都只是對代碼作了一點簡單的封裝,二者能夠單獨使用,並非說使用了createAction就必需要用handleAction。
還記得上面在使用redux-actions的createAction時,咱們對異步的action沒法處理。
由於咱們使用createAction後返回的是一個對象,而不是一個函數,就會致使redux-thunk的代碼沒有起到做用。
而如今咱們將使用redux-promise來處理這類狀況。
能夠看看以前咱們使用 createAction的例子:
export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);
如今咱們先加入redux-promise中間件:
import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers'; const store = createStore(reducer, applyMiddleware(thunk, createLogger, promiseMiddleware));
而後再處理異步action:
export const changeBtnTextAsync = createAction(T.CHANGE_BTN_TEXT_ASYNC, (text) => { return axios.get(`http://test.com/${text}`); });
能夠看到咱們這裏返回的是一個Promise對象.(axios的get方法結果就是Promise對象)
咱們還記得redux-thunk中間件,它會去判斷action是不是一個函數,若是是就執行。
而咱們這裏的redux-promise中間件,他會在dispatch時,判斷若是action不是相似
{ type:'', payload: '' }
這樣的結構,也就是 FSA,那麼就去判斷是否爲promise對象,若是是就執行action.then的玩法。
很明顯,咱們createAction後的結果是FSA,因此會走下面這個分支,它會去判斷action.payload是否爲promise對象,是的話那就
action.payload .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
也就是說咱們的代碼最後會轉變爲:
axios.get(`http://test.com/${text}`) .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
這個中間件的代碼也很簡單,總共19行,你們能夠在github上直接看看。
咱們的異步處理用的是redux-thunk + redux-actions + redux-promise,其實用起來仍是蠻好用的。
可是隨着ES6中Generator的出現,人們發現用Generator處理異步能夠更簡單。
而redux-saga就是用Generator來處理異步。
如下講的知識是基於Generator的,若是您對這個不甚瞭解,能夠簡單瞭解一下相關知識,大概須要2分鐘時間,並不難。
redux-saga文檔並無說本身是處理異步的工具,而是說用來處理邊際效應(side effects),這裏的邊際效應你能夠理解爲程序對外部的操做,好比請求後端,好比操做文件。
redux-saga一樣是一個redux中間件,它的定位就是經過集中控制action,起到一個相似於MVC中控制器的效果。
同時它的語法使得複雜異步操做不會像promise那樣出現不少then的狀況,更容易進行各種測試。
這個東西有它的好處,一樣也有它很差的地方,那就是比較複雜,有必定的學習成本。
而且我我的而言很不習慣Generator的用法,以爲Promise或者await更好用。
這裏仍是記錄一下用法,畢竟有不少框架都用到了這個。
應用這個中間件和咱們的其餘中間件沒有區別:
import React from 'react'; import { createStore, applyMiddleware } from 'redux'; import promiseMiddleware from 'redux-promise'; import createSagaMiddleware from 'redux-saga'; import {watchDelayChangeBtnText} from './sagas'; import reducer from './reducers'; const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(promiseMiddleware, sagaMiddleware)); sagaMiddleware.run(watchDelayChangeBtnText);
建立sage中間件後,而後再將其中間件接入到store中,最後須要用中間件運行sages.js返回的Generator,監控各個action。
如今咱們給出sages.js的代碼:
import { delay } from 'redux-saga'; import { put, call, takeEvery } from 'redux-saga/effects'; import * as T from './components/pageMain/actionTypes'; import { changeBtnText } from './components/pageMain/actions'; const consoleMsg = (msg) => { console.info(msg); }; /** * 處理編輯效應的函數 */ export function* delayChangeBtnText() { yield delay(1000); yield put(changeBtnText('123')); yield call(consoleMsg, '完成改變'); } /** * 監控Action的函數 */ export function* watchDelayChangeBtnText() { yield takeEvery(T.WATCH_CHANGE_BTN_TEXT, delayChangeBtnText); }
在redux-saga中有一類用來處理邊際效應的函數好比put、call,它們的做用是爲了簡化操做。
好比put至關於redux的dispatch的做用,而call至關於調用函數。(能夠參考上面代碼中的例子)
還有另外一類函數就是相似於takeEvery,它的做用就是和普通redux中間件同樣攔截到action後做出相應處理。
好比上面的代碼就是攔截到T.WATCH_CHANGE_BTN_TEXT這個類型的action,而後調用delayChangeBtnText。
而後能夠回看咱們以前的代碼,有這麼一行代碼:
sagaMiddleware.run(watchDelayChangeBtnText);
這裏實際就是引入監控的這個生成器後,再運行監控生成器。
這樣咱們在代碼裏面dispatch類型爲T.WATCH_CHANGE_BTN_TEXT的action時就會被攔截而後作出相應處理。
固然這裏有人可能會提出疑問,難道每個異步都要這麼寫嗎,那豈不是要run不少次?
固然不是這個樣子,咱們能夠在sage中這麼寫:
export default function* rootSaga() { yield [ watchDelayChangeBtnText(), watchOtherAction() ] }
咱們只須要按照這個格式去寫,將watchDelayChangeBtnText這樣用於監控action的生成器放在上面那個代碼的數組中,而後做爲一個生成器返回。
如今只須要引用這個rootSaga便可,而後run這個rootSaga。
之後若是要監控更多的action,只須要在sages.js中加上新的監控的生成器便可。
經過這樣的處理,咱們就將sages.js作成了一個像MVC中的控制器的東西,能夠用來處理各類各樣的action,處理複雜的異步操做和邊際效應。
可是這裏要注意,必定要加以區分sages.js中使用監控的action和真正功能用的action,好比加個watch關鍵字,以避免業務複雜後代碼混亂。
總的來講:
OK,雖說不想寫那麼多,結果仍是寫了一大堆。
若是您以爲對您還有幫助,那麼也請點個贊吧。