最近幾天對 redux 的中間件進行了一番梳理,又看了 redux-saga 的文檔,和 redux-thunk 和 redux-promise 的源碼,結合前段時間看的redux的源碼的一些思考,感受對 redux 中間件的有了更加深入的認識,所以總結一下。react
Redux自己就提供了很是強大的數據流管理功能,但這並非它惟一的強大之處,它還提供了利用中間件來擴展自身功能,以知足用戶的開發需求。首先咱們來看中間件的定義:git
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.json
這是Dan Abramov 對 middleware 的描述。簡單來說,Redux middleware 提供了一個分類處理 action 的機會。在 middleware 中,咱們能夠檢閱每個流過的 action,並挑選出特定類型的 action 進行相應操做,以此來改變 action。這樣提及來可能會有點抽象,咱們直接來看圖,這是在沒有中間件狀況下的 redux 的數據流:redux
上面是很典型的一次 redux 的數據流的過程,但在增長了 middleware 後,咱們就能夠在這途中對 action 進行截獲,並進行改變。且因爲業務場景的多樣性,單純的修改 dispatch 和 reduce 顯然不能知足你們的須要,所以對 redux middleware 的設計理念是能夠自由組合,自由插拔的插件機制。也正是因爲這個機制,咱們在使用 middleware 時,咱們能夠經過串聯不一樣的 middleware 來知足平常的開發需求,每個 middleware 均可以處理一個相對獨立的業務需求且相互串聯:api
如上圖所示,派發給 redux Store 的 action 對象,會被 Store 上的多箇中間件依次處理,若是把 action 和當前的 state 交給 reducer 處理的過程看作默認存在的中間件,那麼其實全部的對 action 的處理均可以有中間件組成的。值得注意的是這些中間件會按照指定的順序依次處理傳入的 action,只有排在前面的中間件完成任務後,後面的中間件纔有機會繼續處理 action,一樣的,每一箇中間件都有本身的「熔斷」處理,當它認爲這個 action 不須要後面的中間件進行處理時,後面的中間件就不能再對這個 action 進行處理了。promise
而不一樣的中間件之因此能夠組合使用,是由於 Redux 要求全部的中間件必須提供統一的接口,每一箇中間件的尉氏縣邏輯雖然不同,但只要遵循統一的接口就能和redux以及其餘的中間件對話了。app
因爲redux 提供了 applyMiddleware 方法來加載 middleware,所以咱們首先能夠看一下 redux 中關於 applyMiddleware 的源碼:異步
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利用傳入的createStore和reducer和建立一個store
const store = createStore(...args)
let dispatch = () => {
throw new Error(
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 讓每一個 middleware 帶着 middlewareAPI 這個參數分別執行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 接着 compose 將 chain 中的全部匿名函數,組裝成一個新的函數,即新的 dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製代碼
咱們能夠看到applyMiddleware的源碼很是簡單,但卻很是精彩,具體的解讀能夠看個人這篇文章: redux源碼解讀async
從上面的代碼咱們不難看出,applyMiddleware 這個函數的核心就在於在於組合 compose,經過將不一樣的 middlewares 一層一層包裹到原生的 dispatch 之上,而後對 middleware 的設計採用柯里化的方式,以便於compose ,從而能夠動態產生 next 方法以及保持 store 的一致性。ide
提及來可能有點繞,直接來看一個啥都不幹的中間件是如何實現的:
const doNothingMidddleware = (dispatch, getState) => next => action => next(action)
複製代碼
上面這個函數接受一個對象做爲參數,對象的參數上有兩個字段 dispatch 和 getState,分別表明着 Redux Store 上的兩個同名函數,但須要注意的是並非全部的中間件都會用到這兩個函數。而後 doNothingMidddleware 返回的函數接受一個 next 類型的參數,這個 next 是一個函數,若是調用了它,就表明着這個中間件完成了本身的職能,並將對 action 控制權交予下一個中間件。但須要注意的是,這個函數還不是處理 action 對象的函數,它所返回的那個以 action 爲參數的函數纔是。最後以 action 爲參數的函數對傳入的 action 對象進行處理,在這個地方能夠進行操做,好比:
在具備上面這些功能後,一箇中間件就足夠獲取 Store 上的全部信息,也具備足夠能力可用之數據的流轉。看完上面這個最簡單的中間件,下面咱們來看一下 redux 中間件內,最出名的中間件 redux-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;
複製代碼
redux-thunk的代碼很簡單,它經過函數是變成的思想來設計的,它讓每一個函數的功能都儘量的小,而後經過函數的嵌套組合來實現複雜的功能,我上面寫的那個最簡單的中間件也是如此(固然,那是個瓜皮中間件)。redux-thunk 中間件的功能也很簡單。首先檢查參數 action 的類型,若是是函數的話,就執行這個 action 函數,並把 dispatch, getState, extraArgument 做爲參數傳遞進去,不然就調用 next 讓下一個中間件繼續處理 action 。
須要注意的是,每一箇中間件最裏層處理 action 參數的函數返回值都會影響 Store 上的 dispatch 函數的返回值,但每一箇中間件中這個函數返回值可能都不同。就好比上面這個 react-thunk 中間件,返回的多是一個 action 函數,也有可能返回的是下一個中間件返回的結果。所以,dispatch 函數調用的返回結果一般是不可控的,咱們最好不要依賴於 dispatch 函數的返回值。
在多種中間件中,處理 redux 異步事件的中間件,絕對佔有舉足輕重的地位。從簡單的 react-thunk 到 redux-promise 再到 redux-saga等等,都表明這各自解決redux異步流管理問題的方案
前面咱們已經對redux-thunk進行了討論,它經過多參數的 currying 以實現對函數的惰性求值,從而將同步的 action 轉爲異步的 action。在理解了redux-thunk後,咱們在實現數據請求時,action就能夠這麼寫了:
function getWeather(url, params) {
return (dispatch, getState) => {
fetch(url, params)
.then(result => {
dispatch({
type: 'GET_WEATHER_SUCCESS', payload: result,
});
})
.catch(err => {
dispatch({
type: 'GET_WEATHER_ERROR', error: err,
});
});
};
}
複製代碼
儘管redux-thunk很簡單,並且也很實用,但人老是有追求的,都追求着使用更加優雅的方法來實現redux異步流的控制,這就有了redux-promise。
不一樣的中間件都有着本身的適用場景,react-thunk 比較適合於簡單的API請求的場景,而 Promise 則更適合於輸入輸出操做,比較fetch函數返回的結果就是一個Promise對象,下面就讓咱們來看下最簡單的 Promise 對象是怎麼實現的:
import { isFSA } from 'flux-standard-action';
function isPromise(val) {
return val && typeof val.then === 'function';
}
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}
複製代碼
它的邏輯也很簡單主要是下面兩部分:
結合 redux-promise 咱們就能夠利用 es7 的 async 和 await 語法,來簡化異步操做了,好比這樣:
const fetchData = (url, params) => fetch(url, params)
async function getWeather(url, params) {
const result = await fetchData(url, params)
if (result.error) {
return {
type: 'GET_WEATHER_ERROR', error: result.error,
}
}
return {
type: 'GET_WEATHER_SUCCESS', payload: result,
}
}
複製代碼
redux-saga是一個管理redux應用異步操做的中間件,用於代替 redux-thunk 的。它經過建立 Sagas 將全部異步操做邏輯存放在一個地方進行集中處理,以此將react中的同步操做與異步操做區分開來,以便於後期的管理與維護。對於Saga,咱們可簡單定義以下:
Saga = Worker + Watcher
redux-saga至關於在Redux原有數據流中多了一層,經過對Action進行監聽,從而捕獲到監聽的Action,而後能夠派生一個新的任務對state進行維護(這個看項目自己的需求),經過更改的state驅動View的變動。以下圖所示:
saga特色:
function *getCurrCity(ip) {
const data = yield call('/api/getCurrCity.json', { ip })
yield put({
type: 'GET_CITY_SUCCESS', payload: data,
})
}
function * getWeather(cityId) {
const data = yield call('/api/getWeatherInfo.json', { cityId })
yield put({
type: 'GET_WEATHER_SUCCESS', payload: data,
})
}
function loadInitData(ip) {
yield getCurrCity(ip)
yield getWeather(getCityIdWithState(state))
yield put({
type: 'GET_DATA_SUCCESS',
})
}
複製代碼
總的來說Redux Saga適用於對事件操做有細粒度需求的場景,同時它也提供了更好的可測試性,與可維護性,比較適合對異步處理要求高的大型項目,而小而簡單的項目徹底可使用 redux-thunk 就足以知足自身需求了。畢竟 react-thunk 對於一個項目自己而言,毫無侵入,使用極其簡單,只需引入這個中間件就好了。而 react-saga 則要求較高,難度較大,但勝在優雅(雖然我以爲asycn await的寫法更優雅)。我如今也並無掌握和實踐這種異步流的管理方式,所以較爲底層的東西先就不討論了。
參考資料: