Redux Middleware中間件源碼 分析

做者從2016年開始接觸 React+Redux,經過閱讀Redux源碼,瞭解了其實現原理。git

Redux代碼量很少,結構也很清晰,函數式編程思想貫穿着整個Redux源碼,如純函數,高階函數,Curry,Compose。github

本文首先會介紹函數式編程的思想,再逐步介紹Redux中間件的實現。編程

看完本文,但願能夠幫助你瞭解中間件實現的原理。redux

1) 基本概念

Redux是可預測的狀態管理框架。它很好的解決多交互,多數據源的訴求。閉包

Redux設計理念有三個原則: 1. 單一數據源 2. State只讀 3. 使用純函數變動state值。app

基本概念 原則 解釋
Store (1) 單一數據源 (2) State只讀 Store能夠看作是數據存儲的一個容器。在這個容器裏面,只會維護惟一的一個State Tree。

Store會給定4種基礎操做方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener)框架

根據單一數據源原則,全部數據會經過store.getState()方法調用獲取。dom

根據State只讀原則,數據變動會經過store,dispatch(action)方法。koa

Action (3) 使用純函數變動state值 Action能夠理解爲變動數據的信息載體。type是變動數據的惟一標誌,payload是用來攜帶須要變動的數據。

格式爲:const action = { type: 'xxx', payload: 'yyy' };異步

Reducer (3) 使用純函數變動state值 Reducer是個純函數。負責根據獲取action.type的內容,計算state數值。

reducer: prevState => action => newState。

正常的一個同步數據流爲:view層觸發actionCreator,actionCreator經過store.dispatch(action)方法, 變動reducer。

可是面對多種多樣的業務場景,同步數據流方式顯然沒法知足。對於改變reducer的異步數據操做,就須要用到中間件的概念。如圖所示。

2) 函數式編程

函數式編程貫穿着Redux的核心。這裏會簡單介紹幾個基本概念。若是你已經瞭解了函數式編程的核心技術,例如 高階函數,compose, currying,遞歸,能夠直接繞過這裏。

我簡單理解的函數式編程思想是: 經過函數的拆解,抽象,組合的方式去編程。複雜問題能夠拆解成小粒度函數,最終利用組合函數的調用達成目的。

2.1) 高階函數

Higher order functions can take functions as parameters and return functions as return values.

接受函數做爲參數傳入,並能返回封裝後函數。

2.2) Compose

Composes functions from right to left.

組合函數,將函數串聯起來執行。就像domino同樣,推倒第一個函數,其餘函數也跟着執行。

首先咱們看一個簡單的例子。

// 實現公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
 
// 深度嵌套函數模式 deeply nested function,將全部函數串聯執行起來。
subtract(multiple(add(200)));
複製代碼

上述例子執行結果爲:500

compose 實際上是經過reduce()方法,實現將全部函數的串聯。不直接使用深度嵌套函數模式,加強了代碼可讀性。不要把它想的很難。

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
compose(subtract, multiple, add)(200);複製代碼

2.3) Currying

Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

翻譯過來是:把接受多個參數 的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。

直接擼代碼解釋

// 實現公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
  
// Curring實現 使用一層層包裹的單參匿名函數,來實現多參數函數的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);複製代碼

*Currying只容許接受單參數。

3) Redux applyMiddleware.js

Redux中reducer更關注的是數據邏輯轉化,因此Redux中間件是爲了加強dispatch方法出現的。如咱們上面圖,所描述的流程。中間件調用鏈,會在dispatch(action)方法以前調用。

因此Redux中間件實現核心目標是:改造dispatch方法。

redux對中間件的實現,代碼是很精簡。總體都不超過20行。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []
 
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
 
    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

接下來,一步步的解析Redux在中間件實現的過程。

applyMiddleware.js 方法有三個主要的步驟,以下:

  1. 將全部的中間件組合在一塊兒, 並保證最後一個執行的是dispatch(action)方法。
  2. 像Koa全部中間件對ctx的調用同樣。保證全部的中間件都能訪問到Store。
  3. 最後將含有中間件調用鏈的新dispatch方法,合併到Store中。
  4. redux對中間件的定義格式爲:mid1 = store => next => action => { next(action) };

看到這裏,你可能有這麼幾個疑問?

  1. 如何將全部的middleware串聯執行在一塊兒?並能夠保證最後一個執行的是dispatch方法?
  2. 如何讓全部的中間件均可以訪問到Store?
  3. 由於新造成的dispatch方法,爲含有中間件調用鏈的方法結合。中間件若是調用dispatch,豈不是會死循環在調用鏈中?
  4. 爲何將中間件格式定義爲 mid1 = store => next => action => { next(action) } ?

爲了解決這4個疑問,下面將針對相應問題,逐步解析。

3.1) 中間件串聯

疑問:

  1. 如何將全部的middleware串聯執行在一塊兒?並能夠保證最後一個執行的是dispatch(action)方法?

解決思路:

  1. 深度嵌套函數 / compose組合函數方法,將全部的中間件串聯起來。
  2. 封裝最後一個函數做爲dispatch(action)方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
  1. compose(...)將全部中間件串聯
  2. 定義final做爲最後執行dispatch的函數
*/
compose(final, middleware2, middleware1)(action)
複製代碼

3.2) 中間件可訪問Store

疑問:

  1. 如何讓全部的中間件均可以訪問到Store?

能夠參考咱們對Koa2中間件的定義 const koaMiddleware = async (ctx, next) => { };

解決思路:

  • 給每個middleware傳遞Store, 保證每個中間件訪問到的都是一致的。
const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);
複製代碼

若是咱們想使用compose方法,將全部中間件串聯起來,那就必須傳遞單一參數。

根據上面函數式編程講到的currying方法,對每一箇中間件柯里化處理。

// 柯里化處理參數
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
 
// 將store保存在各個函數中 -> 循環執行處理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);
複製代碼

經過循環處理,將store內容,傳遞給全部中間件。這裏就體現了currying的做用,延遲計算和參數複用。

3.3) 中間件調用新dispatch方法死循環

疑問:

  1. 由於新造成的dispatch方法,爲含有中間件調用鏈的方法結合。中間件若是調用dispatch,豈不是會死循環在調用鏈中?
new_dispatch = compose(...chain)(store.dispatch);   

new_store = { ...store, dispatch: new_dispatch };
複製代碼

根據源碼的解析,新和成new_dispatch是帶有中間件調用鏈的新函數,並非原來使用的store.dispatch方法。

若是根據3.2) 例子使用的方式傳入store, const chain = [final, middleware2, middleware1].map(midItem => midItem(store));

此時保存在各個中間件中的store.dispatch爲已組合中間件dispatch方法,中間件若是調用dispatch方法,會發生死循環在調用鏈中。

根據上述文字的描述,右圖是死循環的說明。

解決思路:

  1. 給定全部中間件的dispatch方法爲原生store.dispatch方法,不是新和成的dispatch方法。
// 這就是爲何在給全部middleware,共享Store的時候,會從新定義一遍getState和dispatch方法。

const middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => dispatch(action)
}

chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
複製代碼

3.4) 保證中間件不斷裂

疑問:

  1. 爲何將中間件格式定義爲 mid1 = store => next => action => { next(action) } ?

上述例子有提到每次都會返回action給下一個中間件,例如 const middleware1 = store => action => action;

如何保證中間件不會由於沒有傳遞action而斷裂?

這裏必須說明的是:Koa中間件能夠經過調用await next()方法,繼續執行下一個中間件,也能夠中斷當前執行,好比 ctx.response.body = ‘xxxx’ (直接中斷下面中間件的執行)。

通常狀況下,Redux不容許調用鏈中斷,由於咱們最終須要改變state內容。(* 好比redux-thunk使用有意截斷的除外)。

解決思路:

  1. 若是能夠保證,上一個中間件都有下一個中間件的註冊,相似Koa對下一個中間件調用方式next(),不就能夠保證了中間件不會斷裂。
// 柯里化處理參數
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
 
// 中間件串聯
const chain = [middleware1, middleware2 ].map(midItem => midItem({
  dispatch: (action) => store.dispatch(action)}));
 
// compose(...chain)會造成一個調用鏈, next指代下一個函數的註冊, 若是執行到了最後next就是原生的store.dispatch方法
dispatch = compose(...chain)(store.dispatch);
複製代碼

4) 總結

Redux applyMiddleware.js機制的核心在於,函數式編程的compose組合函數,需將全部的中間件串聯起來。

爲了配合compose對單參函數的使用,對每一箇中間件採用currying的設計。同時,利用閉包原理作到每一箇中間件共享Store。

另外,Redux / React應用函數式編程思想設計,實際上是經過組合和抽象來減低軟件管理複雜度。

簡單寫了個學習例子 參考 https://github.com/Linjiayu6/learn-redux-code, 若是有幫助到你,點個贊 咩~

簡歷請投遞至郵箱linjiayu@meituan.com

相關文章
相關標籤/搜索