天天看點

深入了解Redux中間件的原理

1、redux中間件簡介

1.1、什麼是redux中間件

redux 提供了類似後端 Express 的中間件概念,本質的目的是提供第三方插件的模式,自定義攔截 action -> reducer 的過程。變為 action -> middlewares -> reducer 。這種機制可以讓我們改變資料流,實作如異步 action ,action 過濾,日志輸出,異常報告等功能。

通俗來說,redux中間件就是對dispatch的功能做了擴充。

先來看一下傳統的redux執行流程:

深入了解Redux中間件的原理

圖1 redux傳統執行流程

代碼示例:

import { createStore } from 'redux';

/**
 * 這是一個 reducer,形式為 (state, action) => state 的純函數。
 * 描述了 action 如何把 state 轉變成下一個 state。
 */
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}

// 建立 Redux store 來存放應用的狀态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手動訂閱更新,也可以事件綁定到視圖層。
store.subscribe(() =>
  console.log(store.getState())
);

// 改變内部 state 惟一方法是 dispatch 一個 action。
// action 可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
           

Redux的核心概念其實很簡單:将需要修改的state都存入到store裡,發起一個action用來描述發生了什麼,用reducers描述action如何改變state tree 。建立store的時候需要傳入reducer,真正能改變store中資料的是store.dispatch API。

對dispatch改造後,效果如下:

深入了解Redux中間件的原理

圖2 dispatch改造後的執行流程

如上圖所示,dispatch派發給 redux Store 的 action 對象,到達reducer之前,進行一些額外的操作,會被 Store 上的多個中間件依次處理。例如可以利用中間件來進行日志記錄、建立崩潰報告、調用異步接口或者路由等等,那麼其實所有的對 action 的處理都可以有中間件組成的。 簡單來說,中間件就是對store.dispatch()的增強。

1.2、使用redux中間件

redux有很多中間件,我們這裡以

redux-thunk

為例。

代碼示例:

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
 const store = createStore(
  reducers, 
  applyMiddleware(thunk)
);
           

直接将thunk中間件引入,放在applyMiddleware方法之中,傳入createStore方法,就完成了store.dispatch()的功能增強。即可以在reducer中進行一些異步的操作。

Redux middleware 提供了一個分類處理 action 的機會。在 middleware 中,我們可以檢閱每一個流過的 action,并挑選出特定類型的 action 進行相應操作,以此來改變 action。其實applyMiddleware就是Redux的一個原生方法,将所有中間件組成一個數組,依次執行。

中間件多了可以當做參數依次傳進去。

代碼示例:

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';

const logger = createLogger();

const store = createStore(
  reducers, 
  applyMiddleware(thunk, logger) //會按順序執行
);
           

2、中間件的運作機制

2.1、createStore源碼分析

源碼:

// 摘至createStore
export function createStore(reducer, rootState, enhance) {
    //...
    
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
          throw new Error('Expected the enhancer to be a function.')
        }
       /*
        若使用中間件,這裡 enhancer 即為 applyMiddleware()
        若有enhance,直接傳回一個增強的createStore方法,可以類比成react的高階函數
       */
       return enhancer(createStore)(reducer, preloadedState)
  }
    
  //...
}
           

對于createStore的源碼我們隻需要關注和applyMiddleware有關的地方, 通過源碼得知在調用createStore時傳入的參數進行一個判斷,并對參數做矯正。 據此可以得出createStore有多種使用方法,根據第一段參數判斷規則,我們可以得出createStore的兩種使用方式:

或:

經過createStore中的第一個參數判斷規則後,對參數進行了校正,得到了新的enhancer得值,如果新的enhancer的值不為undeifined,便将createStore傳入enhancer(即applyMiddleware調用後傳回的函數)内,讓enhancer執行建立store的過程。也就時說這裡的:

實際上等同于:

applyMiddleware會有兩層柯裡化,同時表明它還有一種很函數式程式設計的用法,即 :

這種方式将建立store的步驟完全放在了applyMiddleware内部,并在其内第二層柯裡化的函數内執行建立store的過程即調用createStore,調用後程式将跳轉至createStore走參數判斷流程最後再建立store。

無論哪一種執行createStore的方式,我們都終将得到store,也就是在creaeStore内部最後傳回的那個包含dispatch、subscribe、getState等方法的對象。

2.2、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
    }
  }
}
           

為友善閱讀和了解,部分ES6箭頭函數已修改為ES5的普通函數形式,如下:

function applyMiddleware (...middlewares){
    return function (createStore){
        return function (reducer, preloadedState, enhancer){
            const store = createStore(reducer, preloadedState, enhancer);
            let dispatch = function (){
                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 這個函數的核心就在于在于組合 compose,通過将不同的 middlewares 一層一層包裹到原生的 dispatch 之上,然後對 middleware 的設計采用柯裡化的方式,以便于compose ,進而可以動态産生 next 方法以及保持 store 的一緻性。

在函數式程式設計(Functional Programming)相關的文章中,經常能看到 柯裡化(Currying)這個名詞。它是數學家柯裡(Haskell Curry)提出的。

柯裡化,用一句話解釋就是,把一個多參數的函數轉化為單參數函數的方法。

根據源碼,我們可以将其主要功能按步驟劃分如下:

1、依次執行

middleware

middleware

執行後傳回的函數合并到一個

chain

數組,這裡我們有必要看看标準

middleware

的定義格式,如下:

周遊所有的中間件,并調用它們,傳入那個類似于store的對象middlewareAPI,這會導緻中間件中第一層柯裡化函數被調用,并傳回一個接收next(即dispatch)方法作為參數的新函數。

export default store => next => action => {}

// 即
function (store) {
    return function(next) {
        return function (action) {
            return {}
        }
    }
}
           

那麼此時合并的

chain

結構如下:

[    ...,
    function(next) {
        return function (action) {
            return {}
        }
    }
]
           

2、改變

dispatch

指向

我們展開了這個數組,并将其内部的元素(函數)傳給了compose函數,compose函數又傳回了我們一個新函數。然後我們再調用這個新函數并傳入了原始的未經任何修改的dispatch方法,最後傳回一個經過了修改的新的dispatch方法。

什麼是compose?在函數式程式設計中,compose指接收多個函數作為參數,并傳回一個新的函數的方式。調用新函數後傳入一個初始的值作為參數,該參數經最後一個函數調用,将結果傳回并作為倒數第二個函數的入參,倒數第二個函數調用完後,将其結果傳回并作為倒數第三個函數的入參,依次調用,知道最後調用完傳入compose的所有的函數後,傳回一個最後的結果。

compose

函數如下:

[...chain].reduce((a, b) => (...args) => a(b(...args)))

實際就是一個柯裡化函數,即将所有的

middleware

合并成一個

middleware

,并在最後一個

middleware

中傳入目前的

dispatch

// 假設chain如下:
chain = [
    a: next => action => { console.log('第1層中間件') return next(action) }
    b: next => action => { console.log('第2層中間件') return next(action) }
    c: next => action => { console.log('根dispatch') return next(action) }
]
           

調用

compose(...chain)(store.dispatch)

後傳回

a(b(c(dispatch)))

可以發現已經将所有

middleware

串聯起來了,并同時修改了

dispatch

的指向。

最後看一下這時候compose執行傳回,如下:

dispatch = a(b(c(dispatch)))

// 調用dispatch(action)
// 執行循序
/*
   1. 調用 a(b(c(dispatch)))(action) __print__: 第1層中間件
   2. 傳回 a: next(action) 即b(c(dispatch))(action)
   3. 調用 b(c(dispatch))(action) __print__: 第2層中間件
   4. 傳回 b: next(action) 即c(dispatch)(action)
   5. 調用 c(dispatch)(action) __print__: 根dispatch
   6. 傳回 c: next(action) 即dispatch(action)
   7. 調用 dispatch(action)
*/
           

總結來說就是:

在中間件串聯的時候,middleware1-3的串聯順序是從右至左的,也就是middleware3被包裹在了最裡面,它内部含有對原始的store.dispatch的調用,middleware1被包裹在了最外邊。

當我們在業務代碼中dispatch一個action時,也就是中間件執行的時候,middleware1-3的執行順序是從左至右的,因為最後被包裹的中間件,将被最先執行。

如圖所示:

深入了解Redux中間件的原理

3、常見的redux中間件

3.1、logger日志中間件

源碼:

function createLogger(options = {}) {
  /**
   * 傳入 applyMiddleWare 的函數
   * @param  {Function} { getState      }) [description]
   * @return {[type]}      [description]
   */
  return ({ getState }) => (next) => (action) => {
    let returnedValue;
    const logEntry = {};
    logEntry.prevState = stateTransformer(getState());
    logEntry.action = action;
    // .... 
    returnedValue = next(action);
    // ....
    logEntry.nextState = stateTransformer(getState());
    // ....
    return returnedValue;
  };
}

export default createLogger;
           

為了友善檢視,将代碼修改為ES5之後,如下:

/**
 * getState 可以傳回最新的應用 store 資料
 */
function ({getState}) {
    /**
     * next 表示執行後續的中間件,中間件有可能有多個
     */
    return function (next) {
        /**
         * 中間件處理函數,參數為目前執行的 action 
         */
        return function (action) {...}
    }
}
           

這樣的結構本質上就是為了将 middleware 串聯起來執行。

3.2、redux異步管理中間件

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

3.2.1、redux-thunk

redux-thunk的使用:

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,
                });
            });
        };
}
           

在上述使用執行個體中,我們應用thunk中間到redux後,可以dispatch一個方法,在方法内部我們想要真正dispatch一個action對象的時候再執行dispatch即可,特别是異步操作時非常友善。

源碼:

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;
           

為了友善閱讀,源碼中的箭頭函數在這裡換成了普通函數,如下:

function createThunkMiddleware (extraArgument){
    return function ({dispatch, getState}){
        return function (next){
            return function (action){
                if (typeof action === 'function'){
                    return action(dispatch, getState, extraArgument);
                }
                return next(action);
            };
        }
    }
}

let thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
           

thunk是一個很常用的redux中間件,應用它之後,我們可以dispatch一個方法,而不僅限于一個純的action對象。它的源碼也很簡單,如上所示,除去文法固定格式也就區區幾行。

下面我們就來看看源碼(為了友善閱讀,源碼中的箭頭函數在這裡換成了普通函數),首先是這三層柯裡化:

// 外層
function createThunkMiddleware (extraArgument){
     // 第一層
    return function ({dispatch, getState}){
       // 第二層
        return function (next){
            // 第三層
            return function (action){
                if (typeof action === 'function'){
                    return action(dispatch, getState, extraArgument);
                }
                return next(action);
            };
        }
    }
}
           

首先是外層,上面的源碼可知,這一層存在的主要目的是支援在調用applyMiddleware并傳入thunk的時候時候可以不直接傳入thunk本身,而是先調用包裹了thunk的函數(第一層柯裡化的父函數)并傳入需要的額外參數,再将該函數調用的後傳回的值(也就是真正的thunk)傳給applyMiddleware,進而實作對額外參數傳入的支援,使用方式如下:

const store = createStore(reducer, applyMiddleware(thunk.withExtraArgument({api, whatever})));
           

如果無需額外參數則用法如下:

const store = createStore(reducer, applyMiddleware(thunk));
           

接下來來看第一層,這一層是真正applyMiddleware能夠調用的一層,從形參來看,這個函數接收了一個類似于store的對象,因為這個對象被結構以後擷取了它的dispatch和getState這兩個方法,巧的是store也有這兩方法,但這個對象到底是不是store,還是隻借用了store的這兩方法合成的一個新對象?這個問題在我們後面分析applyMiddleware源碼時,自會有分曉。

再來看第二層,在第二層這個函數中,我們接收的一個名為next的參數,并在第三層函數内的最後一行代碼中用它去調用了一個action對象,感覺有點

dispatch({type: 'XX_ACTION', data: {}})

的意思,因為我們可以懷疑它就是一個dispatch方法,或者說是其他中間件處理過的dispatch方法,似乎能通過這行代碼連結上所有的中間件,并在所有隻能中間件自身邏輯處理完成後,最終調用真實的

store.dispath

去dispatch一個action對象,再走到下一步,也就是reducer内。

最後我們看看第三層,在這一層函數的内部源碼中首先判斷了action的類型,如果action是一個方法,我們就調用它,并傳入dispatch、getState、extraArgument三個參數,因為在這個方法内部,我們可能需要調用到這些參數,至少dispatch是必須的。**這三行源碼才是真正的thunk核心所在。所有中間件的自身功能邏輯也是在這裡實作的。**如果action不是一個函數,就走之前解析第二層時提到的步驟。

3.2.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.2.3、redux-saga

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

Saga = Worker + Watcher

redux-saga相當于在Redux原有資料流中多了一層,通過對Action進行監聽,進而捕獲到監聽的Action,然後可以派生一個新的任務對state進行維護(這個看項目本身的需求),通過更改的state驅動View的變更。如下圖所示:

深入了解Redux中間件的原理

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則要求較高,難度較大。