圖解Redux中middleware的洋蔥模型

fi3ework發表於2018-04-24

原文釋出於我的 GitHub 部落格,歡迎 star ?

前言

最近翻出了之前分析的 applyMiddleware 發現自己又看不懂了?,重新看了一遍原始碼,梳理了洋蔥模型的實現方法,在這裡分享一下。

applyMiddleware原始碼解析

applyMiddleware 函式最短但是最 Redux 最精髓的地方,成功的讓 Redux 有了極大的可擴充空間,在 action 傳遞的過程中帶來無數的“副作用”,雖然這往往也是麻煩所在。 這個 middleware 的洋蔥模型思想是從 koa 的中介軟體拿過來的,用圖來表示最直觀。

上圖之前先上一段用來示例的程式碼(via 中介軟體的洋蔥模型),我們會圍繞這段程式碼理解 applyMiddleware 的洋蔥模型機制:

function M1(store) {
  return function(next) {
    return function(action) {
      console.log('A middleware1 開始');
      next(action)
      console.log('B middleware1 結束');
    };
  };
}

function M2(store) {
  return function(next) {
    return function(action) {
      console.log('C middleware2 開始');
      next(action)
      console.log('D middleware2 結束');
    };
  };
}

function M3(store) {
  return function(next) {
    return function(action) {
      console.log('E middleware3 開始');
      next(action)
      console.log('F middleware3 結束');
    };
  };
}
  
function reducer(state, action) {
  if (action.type === 'MIDDLEWARE_TEST') {
    console.log('======= G =======');  
  }
  return {};
}
  
var store = Redux.createStore(
  reducer,
  Redux.applyMiddleware(
    M1,
    M2,
    M3
  )
);

store.dispatch({ type: 'MIDDLEWARE_TEST' });
複製程式碼

再放上 Redux 的洋蔥模型的示意圖(via 中介軟體的洋蔥模型),以上程式碼中介軟體的洋蔥模型如下圖:

            --------------------------------------
            |            middleware1              |
            |    ----------------------------     |
            |    |       middleware2         |    |
            |    |    -------------------    |    |
            |    |    |  middleware3    |    |    |
            |    |    |                 |    |    |
          next next next  ———————————   |    |    |
dispatch  —————————————> |  reducer  | — 收尾工作->|
nextState <————————————— |     G     |  |    |    |
            | A  | C  | E ——————————— F |  D |  B |
            |    |    |                 |    |    |
            |    |    -------------------    |    |
            |    ----------------------------     |
            --------------------------------------


順序 A -> C -> E -> G -> F -> D -> B
    \---------------/   \----------/
            ↓                ↓
      更新 state 完畢      收尾工作
複製程式碼

我們將每個 middleware 真正帶來副作用的部分(在這裡副作用是好的,我們需要的就是中介軟體的副作用),稱為M?副作用,它的函式簽名是 (action) => {}(記住這個名字)。

image

對這個示例程式碼來說,Redux 中介軟體的洋蔥模型執行過程就是:

使用者派發 action → action 傳入 M1 副作用 → 列印 A → 執行 M1 的 next(這個 next 指向 M2 副作用)→ 列印 C → 執行 M2 的 next(這個 next 指向 M3 副作用)→ 列印 E → 執行 M3 的 next(這個 next 指向store.dispatch)→ 執行完畢返回到 M3 副作用列印 F → 返回到 M2 列印 E → 返回到 M1 副作用列印 B -> dispatch 執行完畢。

那麼問題來了,M1 M2 M3的 next 是如何繫結的呢?

答:柯里化繫結,一箇中介軟體完整的函式簽名是 store => next => action {},但是最後執行的洋蔥模型只剩下了 action,外層的 store 和 next 經過了柯里化繫結了對應的函式,接下來看一下 next 是如何繫結的。

const store = createStore(...args)
let chain = []
const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI)) // 繫結 {dispatch和getState}
dispatch = compose(...chain)(store.dispatch) // 繫結 next
複製程式碼

關鍵點就是兩句繫結,先來看第一句

chain = middlewares.map(middleware => middleware(middlewareAPI)) // 繫結 {dispatch和getState}

為什麼要繫結 getState?因為中介軟體需要隨時拿到當前的 state,為什麼要拿到 dispatch?因為中介軟體中可能會存在派發 action 的行為(比如 redux-thunk),所以用這個 map 函式柯里化繫結了 getStatedispatch

此時 chain = [(next)=>(action)=>{…}, (next)=>(action)=>{…}, (next)=>(action)=>{…}] 裡閉包引用著 dispatchgetState

接下來 dispatch = compose(...chain)(store.dispatch),先了解一下 compose 函式

compose(A, B, C)(arg) === A(B(C(arg)))
複製程式碼

這就是 compose 的作用,從右至左依次將右邊的返回值作為左邊的引數傳入,層層包裹起來,在 React 中巢狀 Decorator 就是這麼寫,比如:

compose(D1, D2, D3)(Button)
// 層層包裹後的元件就是
<D1>
    <D2>
        <D3>
        	<Button />
        </D3>
    </D2>
</D1>
複製程式碼

再說回 Redux

dispatch = compose(...chain)(store.dispatch) 
複製程式碼

在例項程式碼中相當於

dispatch = MC1(MC2(MC3(store.dispatch)))
複製程式碼

MC就是 chain 中的元素,沒錯,這又是一次柯里化。

image

至此,真相大白,dispatch 做了一點微小的貢獻,一共幹了兩件事:1. 繫結了各個中介軟體的 next。2. 暴露出一個介面用來接收 action。其實說了這麼多,middleware 就是在自定義一個dispatch,這個 dispatch 會按照洋蔥模型來進行 pipe。

OK,到現在我們已經拿到了想要的 dispatch,返回就可以收工了,來看最終執行的靈魂一圖流:

wx20180424-001706 2x

細節

然而可達鴨眉頭一皺,發現事情還沒這麼簡單,有幾個問題要想一下

dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
複製程式碼

在這裡 dispatch 使用匿名函式是為了能在 middleware 中呼叫 compose 的最新的 dispatch(閉包),必須是匿名函式而不是直接寫成 store.dispatch。

如果直接寫成 store.dispatch,那麼在某個 middleware(除最後一個,最後一個middleware拿到的是原始的 store.dispatch)dispatch 一個 action,比如 redux-thunk

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

    return next(action);
  };
}
複製程式碼

就是攔截函式型別的 action,再能夠對函式形式的 action(其實是個 actionCreator)暴露 API 再執行一次,如果這個 actionCreator 是多層函式的巢狀,則必須每次執行 actionCreator 後的 actionCreator 都可以引用最新的 dispatch 才行。如果不寫成匿名函式,那這個 actionCreator 又走了沒有經過任何中介軟體修飾的 store.dispatch,這顯然是不行的。所以要寫成匿名函式的閉包引用。

還有,這裡使用了 ...args 而不是 action,是因為有個 PR,這個 PR 的作者認為在 dispatch 時需要提供多個引數,像這樣 dispatch(action, option) ,這種情況確實存在,但是隻有當這個需提供多引數的中介軟體是第一個被呼叫的中介軟體時(即在 middlewares 陣列中排最後)才肯定有效 ,因為無法保證上一個呼叫這個多引數中介軟體的中介軟體是使用的 next(action) 或是 next(...args) 來呼叫,所以被改成了 next(…args) ,在這個 PR 的討論中可以看到 Dan 對這個改動持保留意見(但他還是改了),這個改動其實真的挺蛋疼的,我作為一個純良的第三方中介軟體,怎麼能知道你上箇中介軟體傳了什麼亂七八糟的屬性呢,再說傳了我也不知道是什麼意思啊大哥。感覺這就是為了某些 middleware 能夠配合使用,不想往 action 里加東西,就加在引數中了,到底是什麼引數只有這些有約定好引數的 middleware 才能知道了。

redux-logger

Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions (#20).

要求必須把自己放在 middleware 的最後一個,理由是

Otherwise it'll log thunks and promises but not actual actions.

試想,logger 想 log 什麼?就是 store.dispatch 時的資訊,所以 logger 肯定要在 store.dispatch 的前後 console,還記不記得上面哪個中介軟體拿到了 store.dispatch,就是最後一個,如果把 logger 放在第一個的話你就能打出所有的 action 了,比如 redux-thunk 的 actionCreator,列印的數量肯定比放在最後一個多,因為並不是所有的 action 都能走到最後,也有新的 action 在 middleware 在中間被派發。

參考

redux middleware 詳解

Redux 進階教程

redux applyMiddleware 原理剖析

繪圖

ProcessOn

相關文章