深入理解 Redux 中介軟體

Srtian發表於2018-06-15

前言

最近幾天對 redux 的中介軟體進行了一番梳理,又看了 redux-saga 的文件,和 redux-thunk 和 redux-promise 的原始碼,結合前段時間看的redux的原始碼的一些思考,感覺對 redux 中介軟體的有了更加深刻的認識,因此總結一下。

一、Redux中介軟體機制

Redux本身就提供了非常強大的資料流管理功能,但這並不是它唯一的強大之處,它還提供了利用中介軟體來擴充套件自身功能,以滿足使用者的開發需求。首先我們來看中介軟體的定義:

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

這是Dan Abramov 對 middleware 的描述。簡單來講,Redux middleware 提供了一個分類處理 action 的機會。在 middleware 中,我們可以檢閱每一個流過的 action,並挑選出特定型別的 action 進行相應操作,以此來改變 action。這樣說起來可能會有點抽象,我們直接來看圖,這是在沒有中介軟體情況下的 redux 的資料流:

輸入圖片說明
redux.png

上面是很典型的一次 redux 的資料流的過程,但在增加了 middleware 後,我們就可以在這途中對 action 進行截獲,並進行改變。且由於業務場景的多樣性,單純的修改 dispatch 和 reduce 顯然不能滿足大家的需要,因此對 redux middleware 的設計理念是可以自由組合,自由插拔的外掛機制。也正是由於這個機制,我們在使用 middleware 時,我們可以通過串聯不同的 middleware 來滿足日常的開發需求,每一個 middleware 都可以處理一個相對獨立的業務需求且相互串聯:

image

如上圖所示,派發給 redux Store 的 action 物件,會被 Store 上的多箇中介軟體依次處理,如果把 action 和當前的 state 交給 reducer 處理的過程看做預設存在的中介軟體,那麼其實所有的對 action 的處理都可以有中介軟體組成的。值得注意的是這些中介軟體會按照指定的順序依次處理傳入的 action,只有排在前面的中介軟體完成任務後,後面的中介軟體才有機會繼續處理 action,同樣的,每個中介軟體都有自己的“熔斷”處理,當它認為這個 action 不需要後面的中介軟體進行處理時,後面的中介軟體就不能再對這個 action 進行處理了。

而不同的中介軟體之所以可以組合使用,是因為 Redux 要求所有的中介軟體必須提供統一的介面,每個中介軟體的尉氏縣邏輯雖然不一樣,但只要遵循統一的介面就能和redux以及其他的中介軟體對話了。

二、理解中間價的機制

由於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原始碼解讀

從上面的程式碼我們不難看出,applyMiddleware 這個函式的核心就在於在於組合 compose,通過將不同的 middlewares 一層一層包裹到原生的 dispatch 之上,然後對 middleware 的設計採用柯里化的方式,以便於compose ,從而可以動態產生 next 方法以及保持 store 的一致性。

說起來可能有點繞,直接來看一個啥都不幹的中介軟體是如何實現的:

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 函式的返回值。

三、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技術棧》

相關文章