React学习笔记——redux里中间件Middleware的运行机理
1、前言
上篇文章中,我們詳細介紹了redux的相關知識和如何使用,最后使用中間件Middleware來幫助我們完成異步操作,如下圖
上面是很典型的一次 redux 的數據流的過程,在增加了 middleware 后,我們就可以在這途中對 action 進行截獲,并進行改變,進行其他操作。
同時,在使用 middleware 時,我們可以通過串聯不同的 middleware 來滿足日常的開發,每一個 middleware 都可以處理一個相對獨立的業務需求且相互串聯。
如上圖所示,派發給 redux Store 的 action 對象,會被 Store 上的多個中間件依次處理,如果把 action 和當前的 state 交給 reducer 處理的過程看做默認存在的中間件,那么其實所有的對 action 的處理都可以有中間件組成的。值得注意的是這些中間件會按照指定的順序一次處理傳入的 action,只有排在前面的中間件完成任務之后,后面的中間件才有機會繼續處理 action,同樣的,每個中間件都有自己的“熔斷”處理,當它認為這個 action 不需要后面的中間件進行處理時,后面的中間件也就不能再對這個 action 進行處理了
下面我們來研究研究Middleware。
2、正文
2.1、redux-thunk源碼
我們以redux-thunk為例,從node_modules文件夾下面找到redux-thunk文件夾,查看其源碼(下圖為redux-thunk源碼,一共12行)
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;可以看出,thunk是createThunkMiddleware()運行的結果,而該函數里面還包裹了3層函數(柯里化),函數一層一層向下執行。
我們將其中的ES6的箭頭函數換成普通函數,再觀察
function createThunkMiddleware (extraArgument){// 第一層/* getState 可以返回最新的應用 store 數據 */return function ({dispatch, getState}){// 第二層/* next 表示執行后續的中間件,中間件有可能有多個 */return function (next){// 第三層/*中間件處理函數,參數為當前執行的 action */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最后兩行源碼可知,這一層存在的主要目的是支持在調用applyMiddleware并傳入thunk的時候可以不直接傳入thunk本身,而是先調用包裹了thunk的函數(第一層柯里化的父函數),并傳入需要的額外參數,再將該函數調用的后返回的值(也就是真正的thunk)傳給applyMiddleware,從而實現對額外參數傳入的支持,使用方式如下:
- 如果無需額外參數則用法如下:
- 接著看第一層,這一層是真正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不是一個函數,就走之前解析第二層時提到的步驟。
2.2、ApplyMiddleware源碼
applyMiddleware函數共十來行代碼,這里將其完整復制出來。
import compose from './compose'export default function applyMiddleware(...middlewares) {return (createStore) => (...args) => {const store = createStore(...args)let dispatch = () => {throw new Error('Dispatching while constructing your middleware is not allowed. ' +'Other middleware would not be applied to this dispatch.')}const middlewareAPI = {getState: store.getState,dispatch: (...args) => dispatch(...args),}// 1、將store對象的基本方法傳遞給中間件并依次調用中間件const chain = middlewares.map((middleware) => middleware(middlewareAPI))// 2、改變dispatch指向,并將最初的dispatch傳遞給composedispatch = compose(...chain)(store.dispatch)return {...store,dispatch,}} }同樣,我們將applyMiddleware的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('Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.')};const middlewareAPI = {getState: store.getState,dispatch: (...args) => dispatch(...args)};// 1、將store對象的基本方法傳遞給中間件并依次調用中間件const chain = middlewares.map(middleware => middleware(middlewareAPI));// 2、改變dispatch指向,并將最初的dispatch傳遞給composedispatch = compose(...chain)(store.dispatch);return {...store,dispatch};}} }從其源碼可以看出,applyMiddleware內部一開始也是兩層柯里化,所以我們看看和applyMiddleware最有關系的createStore的主要源碼。
2.3、CreateStore源碼
在平時業務中,我們創建store時,一般這樣寫
const store = createStore(reducer,initial_state,applyMiddleware(···));或者
const store = createStore(reducer, applyMiddleware(...));所以我們也要關注createStore和applyMiddleware的源碼
createStore部分源碼:
// 摘至createStore export function createStore(reducer, preloadedState, enhancer) {...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)}............dispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable,} }對于createStore的源碼我們只需要關注和applyMiddleware有關的地方。從其內部前面一部分代碼來看,其實很簡單,就是對調用createStore時傳入的參數進行一個判斷,并對參數做矯正,再決定以哪種方式來執行后續代碼。據此可以得出createStore有多種使用方法,根據第一段參數判斷規則,我們可以得出createStore的兩種使用方式:
const store = createStore(reducer, {a: 1, b: 2}, applyMiddleware(...));以及
const store = createStore(reducer, applyMiddleware(...));- 根據第一段參數判斷規則,我們可以肯定的是:applyMiddleware返回的一定是一個函數
- 經過createStore中的第一個參數判斷規則后,對參數進行了校正,得到了新的enhancer得值:如果新的enhancer的值不為undeifined,便將createStore傳入enhancer(即applyMiddleware調用后返回的函數)內,讓enhancer執行創建store的過程。也就時說這里的:
實際上等同于:
applyMiddleware(mdw1, mdw2, mdw3)(createStore)(reducer, preloadedState);這也解釋了為啥applyMiddleware會有兩層柯里化,同時表明它還有一種很函數式編程的用法,即 :
const store = applyMiddleware(mdw1, mdw2, mdw3)(createStore);這種方式將創建store的步驟完全放在了applyMiddleware內部,并在其內第二層柯里化的函數內執行創建store的過程即調用createStore,調用后程序將跳轉至createStore走參數判斷流程最后再創建store。
無論哪一種執行createStore的方式,我們都終將得到store,也就是在creaeStore內部最后返回的那個包含dispatch、subscribe、getState等方法的對象。
2.4、回看ApplyMiddleware源碼
對于applyMiddleware開頭的兩層柯里化的出現原因以及和createStore有關的方面,在前面分析過。同時,我們之前在redux-thunk里的第一層柯里化中猜測傳入的對象是一個類似于store的對象,通過上個章節中applyMiddleware的確實可以確認了。
這里我們主要討論中間件是如何通過applyMiddleware的工作起來并實現挨個串聯的。
接下來這幾段代碼是整個applyMiddleware的核心部分,也解釋了在第二章節中,我們對thunk中間件為啥有三層柯里化的疑慮
// ... // 1、將store對象的基本方法傳遞給中間件并依次調用中間件 const chain = middlewares.map(middleware => middleware(middlewareAPI)); // 2、改變dispatch指向,并將最初的dispatch傳遞給compose dispatch = compose(...chain)(store.dispatch);return {...store,dispatch }; // ...- 首先,我們可以直觀的看到,applyMiddleware的執行結果最終返回的是:store的所有方法和一個dispatch方法。
2.4.1、redux-thunk的第一層柯里化
這個dispatch方法是怎么來的呢?我們來看頭兩行代碼,這兩行代碼也是所有中間件被串聯起來的核心部分實現,它們也決定了中間件內部為啥會有我們在之前章節中提到的三層柯里化的固定格式,先看第一行代碼:
const chain = middlewares.map(middleware => middleware(middlewareAPI));- 遍歷所有的中間件,并調用它們,傳入那個類似于store的對象middlewareAPI,這會導致中間件(redux-thunk)中第一層柯里化函數被調用,并返回一個接收next(即dispatch)方法作為參數的新函數
- 這一層柯里化主要原因,還是考慮到中間件內部會有調用store方法的需求,所以我們需要在此注入相關的方法,其內存函數可以通過閉包的方式來獲取并調用,若有需要的話
- 遍歷結束以后,我們拿到了一個包含所有中間件新返回的函數的一個數組,將其賦值給變量chain,譯為函數鏈
2.4.2、redux-thunk的第二層柯里化
再來看第二句代碼:
dispatch = compose(...chain)(store.dispatch);- 我們展開了這個數組,并將其內部的元素(函數)傳給了compose函數,compose函數又返回了我們一個新函數。然后我們再調用這個新函數并傳入了原始的未經任何修改的dispatch方法,最后返回一個經過了修改的新的dispatch方法
- 先說一句,compose是從右到左依次調用傳入其內部的函數鏈
- thunk中間件的第二層柯里化函數即在compose內部被調用,并接收了經其右邊那個中間函數改造并返回dispatch方法作為入參,并返回一個新的函數,再在該函數內部添加自己的邏輯,最后調用右邊那個中間函數改造并返回dispatch方法接著執行前一個中間件的邏輯(當然如果只有一個thunk中間件被應用了,或者他出入傳入compose時的最后一個中間件,那么傳入的dispatch方法即為原始的store.dispatch方法)
2.4.3、redux-thunk的第三層柯里化
thunk的第三層柯里化函數,即為被thunk改造后的dispatch方法:
// ... return function (action){// thunk的內部邏輯if (typeof action === 'function'){return action(dispatch, getState, extraArgument);}// 調用經下一個中間件(在compose中為之前的中間件)改造后的dispatch方法(本層洋蔥殼的下一層),并傳入actionreturn next(action); }; // ...- 這個改造后的dispatch函數將通過compose傳入thunk左邊的那個中間件作為入參
2.4.4、總結
經上述分析,我們可以得出一個中間件的串聯和執行時的流程,以下面這段使用applyMiddleware的代碼為例:
export default createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));- 在applyMiddlware內部的compose串聯中間件時,順序是從右至左,就是先調用middleware3、再middleware2、最后middleware1
- middleware3最開始接收真正的store.dispatch作為入參,并返回改造的的dispatch函數作為入參傳給middleware2,這個改造后的函數內部包含有對原始store.dispatch的調用。依次內推知道從右到左走完所有的中間件
- 整個過程就像是給原始的store.dispatch方法套上了一層又一層的殼子,最后得到了一個類似于洋蔥結構的東西,也就是下面源碼中的dispatch,這個經過中間件改造并返回的dispatch方法將替換store被展開后的原始的dispatch方法:
- 而原始的store.dispatch就像這洋蔥內部的芯,被覆蓋在了一層又一層的殼的最里面
- 而當我們剝殼的時候,剝一層殼,執行一層的邏輯,即走一層中間件的功能,直至調用藏在最里邊的原始的store.dispatch方法去派發action。這樣一來我們就不需要在每次派發action的時候再寫單獨的代碼邏輯的
如上圖所示:
- 在中間件串聯的時候,middleware1-3的串聯順序是從右至左的,也就是middleware3被包裹在了最里面,它內部含有對原始的store.dispatch的調用,middleware1被包裹在了最外邊
- 在執行業務代碼中dispatch一個action時,也就是中間件執行的時候,middleware1-3的執行順序是從左至右的,因為最后被包裹的中間件,將被最先執行
2.5、總體流程
進過上述分析,我們可以將其主要功能按步驟劃分如下:
1、依次執行middleware:
將middleware執行后返回的函數合并到一個chain數組,這里我們有必要看看標準middleware的定義格式,如下
**加粗樣式**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函數如下:
[...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__: 根dispatch6. 返回 c: next(action) 即dispatch(action)7. 調用 dispatch(action)本博客參考文章:
- Redux的中間件原理分析
- 十分鐘理解Redux中間件
- 理解 redux 中間件
- 詳解redux中間件
總結
以上是生活随笔為你收集整理的React学习笔记——redux里中间件Middleware的运行机理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ER实体关系图
- 下一篇: 数据结构学习笔记------图