深刻理解 Redux 中間件

前言

最近幾天對 redux 的中間件進行了一番梳理,又看了 redux-saga 的文檔,和 redux-thunk 和 redux-promise 的源碼,結合前段時間看的redux的源碼的一些思考,感受對 redux 中間件的有了更加深入的認識,所以總結一下。react

1、Redux中間件機制

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.png

上面是很典型的一次 redux 的數據流的過程,但在增長了 middleware 後,咱們就能夠在這途中對 action 進行截獲,並進行改變。且因爲業務場景的多樣性,單純的修改 dispatch 和 reduce 顯然不能知足你們的須要,所以對 redux middleware 的設計理念是能夠自由組合,自由插拔的插件機制。也正是因爲這個機制,咱們在使用 middleware 時,咱們能夠經過串聯不一樣的 middleware 來知足平常的開發需求,每個 middleware 均可以處理一個相對獨立的業務需求且相互串聯:api

image

如上圖所示,派發給 redux Store 的 action 對象,會被 Store 上的多箇中間件依次處理,若是把 action 和當前的 state 交給 reducer 處理的過程看作默認存在的中間件,那麼其實全部的對 action 的處理均可以有中間件組成的。值得注意的是這些中間件會按照指定的順序依次處理傳入的 action,只有排在前面的中間件完成任務後,後面的中間件纔有機會繼續處理 action,一樣的,每一箇中間件都有本身的「熔斷」處理,當它認爲這個 action 不須要後面的中間件進行處理時,後面的中間件就不能再對這個 action 進行處理了。promise

而不一樣的中間件之因此能夠組合使用,是由於 Redux 要求全部的中間件必須提供統一的接口,每一箇中間件的尉氏縣邏輯雖然不同,但只要遵循統一的接口就能和redux以及其餘的中間件對話了。app

2、理解中間價的機制

因爲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 對象進行處理,在這個地方能夠進行操做,好比:

  • 調動dispatch派發一個新 action 對象
  • 調用 getState 得到當前 Redux Store 上的狀態
  • 調用 next 告訴 Redux 當前中間件工做完畢,讓 Redux 調用下一個中間件
  • 訪問 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 函數的返回值。

3、redux的異步流

在多種中間件中,處理 redux 異步事件的中間件,絕對佔有舉足輕重的地位。從簡單的 react-thunk 到 redux-promise 再到 redux-saga等等,都表明這各自解決redux異步流管理問題的方案

3.1 redux-thunk

前面咱們已經對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。

3.2 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);
  };
}
複製代碼

它的邏輯也很簡單主要是下面兩部分:

  1. 先判斷是否是標準的 flux action。若是不是,那麼判斷是不是 promise, 是的話就執行 action.then(dispatch),不然執行 next(action)。
  2. 若是是, 就先判斷 payload 是不是 promise,若是是的話 payload.then 獲取數據,而後把數據做爲 payload 從新 dispatch({ ...action, payload: result}) ;不是的話就執行 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,
        }
    }
複製代碼

3.3 redux-saga

redux-saga是一個管理redux應用異步操做的中間件,用於代替 redux-thunk 的。它經過建立 Sagas 將全部異步操做邏輯存放在一個地方進行集中處理,以此將react中的同步操做與異步操做區分開來,以便於後期的管理與維護。對於Saga,咱們可簡單定義以下:

Saga = Worker + Watcher

redux-saga至關於在Redux原有數據流中多了一層,經過對Action進行監聽,從而捕獲到監聽的Action,而後能夠派生一個新的任務對state進行維護(這個看項目自己的需求),經過更改的state驅動View的變動。以下圖所示:

image

saga特色:

  1. saga 的應用場景是複雜異步。
  2. 可使用 takeEvery 打印 logger(logger大法好),便於測試。
  3. 提供 takeLatest/takeEvery/throttle 方法,能夠便利的實現對事件的僅關注最近實踐仍是關注每一次實踐的時間限頻。
  4. 提供 cancel/delay 方法,能夠便利的取消或延遲異步請求。
  5. 提供 race(effects),[...effects] 方法來支持競態和並行場景。
  6. 提供 channel 機制支持外部事件。
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的寫法更優雅)。我如今也並無掌握和實踐這種異步流的管理方式,所以較爲底層的東西先就不討論了。

參考資料:

  • 《深刻淺出React和Redux》
  • 《深刻React技術棧》
相關文章
相關標籤/搜索