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