It provides a third-party extension point between dispatching an
action, and the moment it reaches the reducer.css
這是 redux 做者 Dan 對 middleware 的描述,middleware 提供了一個分類處理 action 的機會,在 middleware 中你能夠檢閱每個流過的 action,挑選出特定類型的 action 進行相應操做,給你一次改變 action 的機會。html
上圖表達的是 redux 中一個簡單的同步數據流動場景,點擊 button 後,在回調中 dispatch 一個 action,reducer 收到 action 後,更新 state 並通知 view 從新渲染。單向數據流,看着沒什麼問題。可是,若是須要打印每個 action 信息用來調試,就得去改 dispatch 或者 reducer 代碼,使其具備打印日誌的功能;又好比點擊 button 後,須要先去服務器請求數據,只有等拿到數據後,才能從新渲染 view,此時咱們又但願 dispatch 或者 reducer 擁有異步請求的功能;再好比須要異步請求完數據後,打印一條日誌,再請求數據,再打印日誌,再渲染...git
面對多種多樣的業務需求,單純的修改 dispatch 或 reducer 的代碼顯然不具備普世性,咱們須要的是能夠組合的,自由插拔的插件機制,這一點 redux 借鑑了 koa 裏中間件的思想,koa 是用於構建 web 應用的 NodeJS 框架。另外 reducer 更關心的是數據的轉化邏輯,因此 redux 的 middleware 是爲了加強 dispatch 而出現的。github
上面這張圖展現了應用 middleware 後 redux 處理事件的邏輯,每個 middleware 處理一個相對獨立的業務需求,經過串聯不一樣的 middleware,實現變化多樣的的功能。那麼問題來了:web
middleware 怎麼寫?編程
redux 是如何讓 middlewares 串聯並跑起來的?json
redux 提供了 applyMiddleware 這個 api 來加載 middleware,爲了方便理解,下圖將二者的源碼放在一塊兒進行分析。redux
圖左邊是 logger,打印 action 的 middleware,圖右邊則是 applyMiddleware 的源碼,applyMiddleware 代碼雖然只有二十多行,卻很是精煉,接下來咱們就分四步來深刻解析這張圖。segmentfault
redux 的代碼都是用 ES6/7 寫的,因此不熟悉諸如
store => next => action =>
或...state
的童鞋,能夠先學習下箭頭函數,展開運算符。api
Step. 1 函數式編程思想設計 middleware
middleware 的設計有點特殊,是一個層層包裹的匿名函數,這實際上是函數式編程中的柯里化 curry,一種使用匿名單參數函數來實現多參數函數的方法。applyMiddleware 會對 logger 這個 middleware 進行層層調用,動態地對 store 和 next 參數賦值。
柯里化的 middleware 結構好處在於:
易串聯,柯里化函數具備延遲執行的特性,經過不斷柯里化造成的 middleware 能夠累積參數,配合組合( compose,函數式編程的概念,Step. 2 中會介紹)的方式,很容易造成 pipeline 來處理數據流。
共享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);
Step. 2 給 middleware 分發 store
建立一個普通的 store 經過以下方式:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
上面代碼執行完後,applyMiddleware 函數陸續得到了三個參數,第一個是 middlewares 數組,[md1, 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));
而後讓每一個 middleware 帶着 middlewareAPI 這個參數分別執行一遍,即執行紅色箭頭指向的函數。執行完後,得到 chain 數組,[f1, f2, ... , fx, ...,fn],它保存的對象是圖中綠色箭頭指向的匿名函數,由於閉包,每一個匿名函數均可以訪問相同的 store,即 middlewareAPI。
備註: middlewareAPI 中的 dispatch 爲何要用匿名函數包裹呢?
咱們用 applyMiddleware 是爲了改造 dispatch 的,因此 applyMiddleware 執行完後,dispatch 是變化了的,而 middlewareAPI 是 applyMiddleware 執行中分發到各個 middleware,因此必須用匿名函數包裹 dispatch, 這樣只要 dispatch 更新了, middlewareAPI 中的 dispatch 應用也會發生變化。
Step. 3 組合串聯 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 的代碼不就依次執行了嘛。
Step. 4 在 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。