序言
這里要講的就是一個Redux在React中的應用問題,講一講Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-saga這些包的作用和他們解決的問題。
因為不想把篇幅拉得太長,所以沒有太多源碼分析和語法講解,能怎么簡單就怎么簡單。
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-saga:控制器與更優雅的異步處理
我們的異步處理用的是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);
創建saga中間件后,然后再將其中間件接入到store中,最后需要用中間件運行sagas.js返回的Generator,監控各個action。
現在我們給出sagas.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很多次?
當然不是這個樣子,我們可以在saga中這么寫:
export default function* rootSaga() {
yield [
watchDelayChangeBtnText(),
watchOtherAction()
]
}
我們只需要按照這個格式去寫,將watchDelayChangeBtnText這樣用於監控action的生成器放在上面那個代碼的數組中,然后作為一個生成器返回。
現在只需要引用這個rootSaga即可,然后run這個rootSaga。
以后如果要監控更多的action,只需要在sagas.js中加上新的監控的生成器即可。
通過這樣的處理,我們就將sagas.js做成了一個像MVC中的控制器的東西,可以用來處理各種各樣的action,處理復雜的異步操作和邊際效應。
但是這里要注意,一定要加以區分sagas.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-saga可以起到一個控制器的作用,集中處理邊際效用,並使得異步操作的寫法更優雅。
OK,雖然說不想寫那么多,結果還是寫了一大堆。
如果您覺得對您還有幫助,那么也請點個贊吧。