1. 程式人生 > >react middleware詳解

react middleware詳解

自:https://www.jianshu.com/p/f4166120489b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

為什麼dispatch需要middleware?

middle.png

上圖表達的是 redux 中一個簡單的同步資料流動的場景,點選 button 後,在回撥中 dispatch 一個 action,reducer 收到 action 後,更新 state 並通知 view 重新渲染。單向資料流,看著沒什麼問題。但是,如果需要列印每一個 action 資訊用來除錯,就得去改 dispatch

或者 reducer 程式碼,使其具有列印日誌的功能;又比如點選 button 後,需要先去伺服器請求資料,只有等拿到資料後,才能重新渲染 view,此時我們又希望 dispatch 或者 reducer 擁有非同步請求的功能;再比如需要非同步請求完資料後,列印一條日誌,再請求資料,再列印日誌,再渲染...

面對多種多樣的業務需求,單純的修改 dispatchreducer 的程式碼顯然不具有普世性,我們需要的是可以組合的,自由插拔的外掛機制,這一點 redux 借鑑了 koa 裡中介軟體的思想,koa 是用於構建 web 應用的 NodeJS 框架。另外 reducer 更關心的是資料的轉化邏輯,所以 redux

middleware 是為了增強 dispatch 而出現的。

middle2.png

上面這張圖展示了應用 middleware 後 redux 處理事件的邏輯,每一個 middleware 處理一個相對獨立的業務需求,通過串聯不同的 middleware,實現變化多樣的的功能。那麼問題來了:

* middleware 怎麼寫?
* redux 是如何讓 middlewares 串聯並跑起來的?

理解middleware機制

redux 提供了 applyMiddleware 這個 api 來載入 middleware,為了方便理解,下圖將兩者的原始碼放在一起進行分析。

middle3.png

圖下邊是 logger,列印 action 的 middleware,圖上邊則是 applyMiddleware 的原始碼,applyMiddleware 程式碼雖然只有二十多行,卻非常精煉,接下來我們就分四步來深入解析這張圖。

  • 第一步:函數語言程式設計思想設計 middleware

    middleware 的設計有點特殊,是一個層層包裹的匿名函式,這其實是函數語言程式設計中的柯里化 curry,一種使用匿名單引數函式來實現多引數函式的方法。applyMiddleware 會對 logger 這個 middleware 進行層層呼叫,動態地對 store 和 next 引數賦值。

柯里化的 middleware 結構好處在於:

  1. 易串聯,柯里化函式具有延遲執行的特性,通過不斷柯里化形成的 middleware 可以累積引數,配合組合( compose,函數語言程式設計的概念,Step. 2 中會介紹)的方式,很容易形成 pipeline 來處理資料流。

2.共享store,在 applyMiddleware 執行過程中,store 還是舊的,但是因為閉包的存在,applyMiddleware 完成後,所有的 middlewares 內部拿到的 store 是最新且相同的。

另外,我們可以發現 applyMiddleware 的結構也是一個多層柯里化的函式,藉助 compose , applyMiddleware 可以用來和其他外掛一起加強 createStore 函式.

import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';

const finalCreateStore = compose(
  // Middleware you want to use in development:
  applyMiddleware(d1, d2, d3),
  // Required! Enable Redux DevTools with the monitors you chose
  DevTools.instrument()
)(createStore);
  • 第二步 給 middleware 分發 store

建立一個普通的 store 通過如下方式:

let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);

上面程式碼執行完後,applyMiddleware 函式陸續獲得了三個引數,第一個是 middlewares 陣列,[mid1, mid2, mid3, ...],第二個 next 是 Redux 原生的 createStore,最後一個是 reducer。我們從對比圖中可以看到,applyMiddleware 利用 createStore 和 reducer 建立了一個 store,然後 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變數,middlewareAPI 就是對比圖中紅色箭頭所指向的函式的入參 store。

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    };
    chain = middlewares.map(middleware => middleware(middlewareAPI));

map 方法讓每個 middleware 帶著 middlewareAPI 這個引數分別執行一遍,即執行紅色箭頭指向的函式。執行完後,獲得 chain 陣列,[f1, f2, ... , fx, ...,fn],它儲存的物件是圖中綠色箭頭指向的匿名函式,因為閉包,每個匿名函式都可以訪問相同的 store,即 middlewareAPI。

  • 第三步 組合串聯 middlewares

    dispatch = compose(...chain)(store.dispatch);

    這一層只有一行程式碼,但卻是 applyMiddleware 精華所在。compose 是函數語言程式設計中的組合,compose 將 chain 中的所有匿名函式,[f1, f2, ... , fx, ..., fn],組裝成一個新的函式,即新的 dispatch,當新 dispatch 執行時,[f1, f2, ... , fx, ..., fn],從左到右依次執行( 所以順序很重要)。Redux 中 compose 的實現是下面這樣的,當然實現方式不唯一。

         function compose(...funcs) {
          return arg => funcs.reduceRight((composed, f) => f(composed), arg);
      }
    

    compose(...chain) 返回的是一個匿名函式,函式裡的 funcs 就是 chain 陣列,當呼叫 reduceRight 時,依次從 funcs 陣列的右端取一個函式 fx 拿來執行,fx 的引數 composed 就是前一次 fx+1 執行的結果,而第一次執行的fn(n代表chain的長度)的引數 arg 就是 store.dispatch。所以當 compose 執行完後,我們得到的 dispatch 是這樣的,假設 n = 3。

dispatch = f1(f2(f3(store.dispatch))))

這個時候呼叫新 dispatch,每個 middleware 的程式碼不就依次執行了嘛.

  • 第四步 在 middleware 中呼叫 dispatch 會發生什麼

經過 compose,所有的 middleware 算是串聯起來了,可是還有一個問題,我們有必要挖一挖。在 step 2 時,提到過每個 middleware 都可以訪問 store,即 middlewareAPI 這個變數,所以就可以拿到 store 的 dispatch 方法,那麼在 middleware 中呼叫 store.dispatch()會發生什麼,和呼叫 next() 有區別嗎?

在 step 2 的時候我們解釋過,通過匿名函式的方式,middleware 中 拿到的 dispatch 和最終 compose 結束後的新 dispatch 是保持一致的,所以在middleware 中呼叫 store.dispatch() 和在其他任何地方呼叫效果是一樣的,而在 middleware 中呼叫 next(),效果是進入下一個 middleware。

正常情況下當我們 dispatch 一個 action 時,middleware 通過 next(action) 一層一層處理和傳遞 action 直到 redux 原生的 dispatch。如果某個 middleware 使用 store.dispatch(action) 來分發 action相當於從外層重新來一遍,假如這個 middleware 一直簡單粗暴地呼叫 store.dispatch(action),就會形成無限迴圈了。那麼 store.dispatch(action) 的勇武之地在哪裡?正確的使用姿勢應該是怎麼樣的?舉個例子,需要傳送一個非同步請求到伺服器獲取資料,成功後彈出一個自定義的 Message。這裡我門用到了 redux-thunk 這個作者寫的 middleware。

const thunk = store => next => action =>
  typeof action === 'function' ?
    action(store.dispatch, store.getState) :
    next(action)

redux-thunk 做的事情就是判斷 action 型別是否是函式,若是,則執行 action,若不是,則繼續傳遞 action 到下個 middleware。

針對上面的需求,我們設計了下面的 action:

   const getThenShow = (dispatch, getState) => {
  const url = 'http://xxx.json';

  fetch(url)
  .then(response => {
    dispatch({
      type: 'SHOW_MESSAGE_FOR_ME',
      message: response.json(),
    });
  }, e => {
    dispatch({
      type: 'FETCH_DATA_FAIL',
      message: e,
    });
  });
};

這個時候只要在業務程式碼裡面呼叫 store.dispatch(getThenShow),redux-thunk 就會攔截並執行 getThenShow 這個 action,getThenShow 會先請求資料,如果成功,dispatch 一個顯示 Message 的 action,否則 dispatch 一個請求失敗的 action。這裡的 dispatch 就是通過 redux-thunk middleware 傳遞進來的。

在 middleware 中使用 dispatch 的場景一般是:
接受到一個定向 action,這個 action 並不希望到達原生的 dsipatch,存在的目的是為了觸發其他新的 action,往往用在非同步請求的需求裡。

總結

applyMiddleware 機制的核心在於組合 compose,將不同的 middlewares 一層一層包裹到原生的 dispatch 之上,而為了方便進行 compose,需對 middleware 的設計採用柯里化 curry 的方式,達到動態產生 next 方法以及保持 store 的一致性。由於在 middleware 中,可以像在外部一樣輕鬆訪問到 store, 因此可以利用當前 store 的 state 來進行條件判斷,用 dispatch 方法攔截老的 action 或傳送新的 action。