序言 這裡要講的就是一個Redux在React中的應用問題,講一講Redux,react redux,redux thunk,redux actions,redux promise,redux sage這些包的作用和他們解決的問題。 因為不想把篇幅拉得太長,所以沒有太多源碼分析和語法講解,能怎麼簡單 ...
序言
這裡要講的就是一個Redux在React中的應用問題,講一講Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-sage這些包的作用和他們解決的問題。
因為不想把篇幅拉得太長,所以沒有太多源碼分析和語法講解,能怎麼簡單就怎麼簡單。
Redux
先看看百度百科上面Redux的一張圖:
這是Redux在Github上的介紹:Redux用於js程式,是一個可預測的狀態容器。
在這裡我們首先要明白的是什麼叫可預測?什麼叫狀態容器?
什麼叫狀態?實際上就是變數,對話框顯示或隱藏的變數,一杯奶茶多少錢的變數。
那麼這個狀態容器,實際上就是一個存放這些變數的變數。
你創建了一個全局變數叫Store,然後將代碼中控制各個狀態的變數存放在裡面,那麼現在Store就叫做狀態容器。
什麼叫可預測?
你在操作這個Store的時候,總是用Store.price的方式來設置值,這種操作數據的方式很原始,對於複雜的系統而言永遠都不知道程式在運行的過程中發生了什麼。
那麼現在我們都通過發送一個Action去做修改,而Store在接收到Action後會使用Reducer對Action傳遞的數據做處理,最後應用到Store中。
相對於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的結合:react-redux
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的中間件
之前我們講到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的日誌,我們可以瞭解到代碼實際運行時到底發生了什麼。
redux-thunk:處理非同步action
在上面的代碼中,我們點擊按鈕後,直接修改了按鈕的文本,這個文本是個固定的值。
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-actions:簡化redux的使用
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-promise:redux-actions的好基友,輕鬆創建和處理非同步action
還記得上面在使用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-sage:控制器與更優雅的非同步處理
我們的非同步處理用的是redux-thunk + redux-actions + redux-promise,其實用起來還是蠻好用的。
但是隨著ES6中Generator的出現,人們發現用Generator處理非同步可以更簡單。
而redux-sage就是用Generator來處理非同步。
以下講的知識是基於Generator的,如果您對這個不甚瞭解,可以簡單瞭解一下相關知識,大概需要2分鐘時間,並不難。
redux-sage文檔並沒有說自己是處理非同步的工具,而是說用來處理邊際效應(side effects),這裡的邊際效應你可以理解為程式對外部的操作,比如請求後端,比如操作文件。
redux-sage同樣是一個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-sage中有一類用來處理邊際效應的函數比如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關鍵字,以免業務複雜後代碼混亂。
總結
總的來說:
- redux是一個可預測的狀態容器,
- react-redux是將store和react結合起來,使得數據展示和修改對於react項目而言更簡單
- redux中間件就是在dispatch action前對action做一些處理
- redux-thunk用於對非同步做操作
- redux-actions用於簡化redux操作
- redux-promise可以配合redux-actions用來處理Promise對象,使得非同步操作更簡單
- redux-sage可以起到一個控制器的作用,集中處理邊際效用,並使得非同步操作的寫法更優雅。
OK,雖然說不想寫那麼多,結果還是寫了一大堆。
如果您覺得對您還有幫助,那麼也請點個贊吧。