Redux原始碼解讀 [05] 之 applyMiddleware和compose
首先我們來看看中介軟體的定義:action發起之後的資料流是這樣的–action -> reducer -> store,而加上中介軟體的處理之後,資料流就變成 action -> middlewareA -> middlewareB ->… -> reducer -> store,相當於在抵達Reucer之前 action 可以進行擴充套件,也就是說我們可以控制每一個流過的action,選擇一些我們期待做出修改的action進行響應操作。常用的中介軟體有非同步支援(redux-thunk)、列印日誌(redux-logger)等。
compose 方法
在解讀applyMiddleware之前,先看看Redux另一個API,也是applyMiddleware裡面使用到的方法,compose,程式碼很簡短,但看起來很複雜:
export default 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)))
}
方法接收一個函式陣列,當陣列長度大於1時,才開始化合作用。最難理解的部分是方法的返回,Array.reduce累加器方法(可參考MDN文件說明)函式的第一個引數是callback回撥,表示陣列中的每個元素(從左到右)都應用這個函式,compose中累加器的callback回撥展開來看也就是:
funcs.reduce(function (a, b) {
return (...args) => a(b(...args))
})
其中 a 是上一次執行完callback函式的返回值,b是陣列當前元素處理的值。
遍歷完函式陣列的所有元素後,會返回一個強力函式,第一個陣列元素包裹在最外層,最後一個數組元素包裹在最裡層,這個函式接受任意的引數 …args,並從陣列後面開始向前,逐個執行,執行完之後的返回值作為下一個函式的引數層層傳遞。
看起來有點繞,看一個例子就明白了:
let final = compose(f,g,h)
// final等價於 函式 f(g(h(...args)))
// 此時使用 final 函式的話
// 如隨意傳遞一個引數
final(666)
// 詳細執行過程就是:
// h(666) => 執行函式,獲取返回值 A
// g(A) => 將A作為引數傳遞給g函式執行,獲取返回值 B
// f(B) => 將B作為引數傳遞給f函式執行,獲取返回值 C
// ...以此類推
可能我們現在還不知道compose到底有什麼用,但沒關係,稍後就能看到compose的魔法。
applyMiddleware 預覽
方法簽名
首先來看applyMiddleware的方法簽名:
export default function applyMiddleware(...middlewares)
引數
* …middlewares 函式陣列,也就是一個個中介軟體,不同與其他函式的是,中介軟體函式需要符合一定規則,才能相容redux,發揮作用,具體的規則參考下方的例子。
方法返回
applyMiddleware方法返回一個函式,形如:
return createStore => (...args) => {
//....
return {
//.....
}
}
applyMiddleware 解讀
通常,我們會這樣使用 applyMiddleware 方法:
const store = createStore(
reducer,
applyMiddleware(...middlewares)
)
還記得在方法createStore()中,一旦遇到如上的呼叫形式,就會直接返回如下形式:
return enhancer(createStore)(reducer, preloadedState)
對這行程式碼進行拆分,並進行分析:
// 上面我們可以拆成兩部分來看
const tmp = enhancer(createStore) // 1
return tmp (reducer, preloadedState) //2
// 這裡的 enhancer 就是 applyMiddleware(...middlewares) 返回的函式:
createStore => (...args) => {
//xxxx
return {
//xxx
}
}
也就是說,creatStore方法中一旦遇到了應用中介軟體引數的時候,會依次傳入 createStore(自身),reducer和reloadedState,層層執行,最終返回一個物件,而這個物件具體的內容則由applyMiddleware方法具體定義。
那麼creatStore遇到中介軟體的情況到底返回了什麼,我們接著看看applyMiddleware的詳細程式碼,直接看到內部返回物件的那一個函式中:
return createStore => (...args) => {
/**
* 函式執行到這裡的時候 可以使用的引數有:
* ...middlewares 就是使用 applyMiddleware(...middlewares) 時傳入的中介軟體函式陣列
* createStore = Store.createStore() 方法
* ...args == (reducer, preloadedState)
*/
// 初始化store
const store = createStore(...args)
//xxxx 一系列處理
return {
...store,
dispatch
}
}
我們看到,函式用傳入的引數去初始化了一個Store,接著,經過一系列處理之後,返回了這個store,並用一個dispatch覆蓋了原來store中的dispatch方法。到這裡我們可以發現,creatStore遇到中介軟體的情況的時候,返回值和createStore原有返回的Store物件相同,提供相同的方法,不同的是dispatch被覆蓋了,或者說經過處理之後被增強了。
再接著看函式發生了什麼:
// 這裡定義了一個 dispatch 方法
// 暫時不用關注其用途
// 後續會說道原因
let dispatch = () => {
throw new Error(
`xxxx`
)
}
const middlewareAPI = {
getState: store.getState,
// 為 dispatch 包裝一層 使得dispatch 支援傳入多個引數
dispatch: (...args) => dispatch(...args)
}
這裡聲明瞭middlewareAPI 常量,作用就是Redux暴露給中介軟體的介面,一個getState()函式,來自原生Store,和一個dispatch方法,注意這個方法並非Store原生,稍候會解釋為什麼要這樣處理。
再看下一部分之前,先介紹一個用於非同步處理的Redux中介軟體,redux-thunk,程式碼十分簡單,寥寥幾行,實際上,為了同Redux相容,中介軟體函式的設計標準都大同小異:
export default const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
// 先記住中介軟體的函式簽名和返回,稍候再來詳細講解 reat-thunk 是如何發揮作用的
接著回到 applyMiddleware 的程式碼中,下一行:
const chain = middlewares.map(middleware => middleware(middlewareAPI))
結合上面的redux-thunk,這段程式碼就不難理解,遍歷所有傳入的中介軟體函式,傳入middlewareAPI執行,將返回值放到chain陣列,這段程式碼執行完後,chain中的中介軟體函式大概是這樣子的:
// middlewareAPI 中的兩個引數已經傳入
// 由於閉包的存在,函式內部可以直接使用 getState 和 dispatch
next => action => {
// 中介軟體處理邏輯.....
return next(action)
}
接著應用了compose,這一步是最難理解,同時也是最靈巧的一部分:
dispatch = compose(...chain)(store.dispatch)
// 我們把這一步拆成兩部分
// 第一步
const final = compose(...chain)
// 第二步
dispatch = final (store.dispatch)
如果我們有a,b,c三個中介軟體,那麼第一步程式碼執行完成之後,final是一個這樣的函式:
const final = a(b(c(...args)))
而且a,b,c 三個函式的定義都是統一的形如:
next => action => {
// 中介軟體處理邏輯.....
return next(action)
}
再看第二步,將Store原生的 dispatch 方法作為引數傳入 final 函式 ,得到的結果賦值給 dispatch 變數:
dispatch = a(b(c(store.dispatch)))
// 為了便於理解 我們來分佈執行 a(b(c(store.dispatch)))
// store.dispatch 作為引數 只會傳遞給第一個函式,這裡也就是c
// 其他的函式依次接收上一個函式的返回值作為引數,詳細過程:
store.dispatch 作為 next 引數給 c
c( next ) 執行後返回函式 action =>{ return store.dispatch(action)} 作為 next 引數傳給b
b( next ) 執行後返回函式 action =>{ return next(action)} 作為 next 引數傳給a
a( next ) 執行後返回函式 action =>{ return next(action)}
當過程結束後,變數dispatch就會得到 a 返回的一個這樣的函式:
dispatch = action =>{
return next(action)
}
可能到這裡我們還是不能明白它是如何執行多箇中間件邏輯的,我們來寫一個例子:
// 3箇中間件 a,b,c
let c = ({getState,dispatch}) => next => aciton =>{
console.log('c')
return next(action)
}
let b = ({getState,dispatch}) => next => aciton =>{
console.log('b')
return next(action)
}
let a = ({getState,dispatch}) => next => aciton =>{
console.log('a')
return next(action)
}
// 我們在程式碼中這樣呼叫它們
// 注意呼叫的順序
const store = createStore(reducer,applyMiddleware(a,b,c))
// 隨便觸發一個事件
store.dispath({ type: 'HELLO_WORLD '})
// 此時得到的輸出是
a
b
c
// 也就是首先執行完a的邏輯,next(action)時 跳到 b處理
// 同樣 b 執行到next(action)時 跳到c處理
// c 執行 next(action) ,這個next是Store原生的dispatch,也就是真正發起action的時候
// 最後將原生的dispatch的返回值一一傳遞,返回即可
還記得我們之前說過的 原生Store.dispatch的返回值是什麼嗎?不記得可以重新看一下createStore的程式碼,dispatch的返回值就是action,增強的dispatch方法也同樣層層返回了action,即便應用了中介軟體,入參和返回都是相同的,只是過程不同。
現在,我們就能明白中介軟體的作用和如何生效的了。中介軟體更像是一條鏈子,傳入一個action後,每一箇中間件都可以對action進行處理,經過一個個中介軟體處理後的action,最終使用了原生的Store.dispatch()來發起,應用中介軟體的修改。注意,中介軟體的next(action)是必須的,如果不呼叫next,就會使得中介軟體執行鏈斷開,導致最終不能發起action。
redux-thunk
說明一下,非同步操作的時候,我們期待發起多個action,例如,獲取資料之前發起一個action,正在獲取資料也可以發起一個action,獲取到資料的時候再發起一個action。接下來我們來看看redux-thunk是如何實現支援非同步操作的,在此之前,我們先看看一個應用了redux-thunk的actionCreator的使用例子:
// actionCreator
export const fetchData = ()=> {
return dispatch => {
try {
dispatch({
type: 'FETCH_START',
text: '開始獲取資料'
})
fetch('/somethingdata').then( data =>{
dispatch({
type: 'FETCH_SUCCESS',
result: data
})
})
} catch (err) {}
}
}
我們多次強調,actionCreator返回的必須是action物件,但是這個actionCreator返回的是函式,而且其引數是 dispatch,然後我們在邏輯裡發起一個非同步請求資料的操作,用 dispatch 發起了兩個action,從而支援非同步發起多個action的需求,具體為什麼返回的是一個引數為dispatch的函式,還要看看redux-thunk的具體實現:
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
export default thunk
原來,redux-thunk對返回的action進行了判斷,如果是function型別,也就代表著需要處理非同步邏輯,此時傳入redux提供的middlewareAPI,即getState方法和dispatch方法,執行action函式。注意這裡的返回值不再是next,這裡返回的是actionCreator執行完後的返回值(一般為undefined),所以會中斷中介軟體鏈的執行,原因很簡單,action為函式的時候並非真正發起了action而是為能在action中使用dispatch方法多次發起action。
就是那麼簡單。。。
middlewareAPI中的dispatch
這裡說到了redux提供的middlewareAPI,不知道還記不記得之前說到過,dispatch 有點奇怪:
export default function applyMiddleware(...middlewares) {
//....
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)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
//.....
return{
...store,
dispatch
}
}
}
看到dispatch變數,其實是作為一個閉包變數返回的。變數有兩個賦值的時候,第一次賦值是定義了一個函式,直接丟擲了一個錯誤,第二次則是通過 compose(…chain)(store.disaptch ) 賦值,也就是我們熟悉的增強過後的 dispatch 賦值,那什麼要多此一舉初始化了一個錯誤的輸出函式呢?查看了redux的issuse後我得到了答案:
// 在redux最早的版本中,redux提供的middlewareAPI是這樣的
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => store.dispatch(action)
}// 看到不同了嗎,是直接使用的原生store提供的dispatch
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
但是這樣做就會出現一個問題,例如有個中介軟體是這樣的:
const errorMiddle = ({ dispatch, getState }) => {
//發起action
dispatch({
type: 'ANY_TYPE_ACTION'
})
return next => action => {
return next(action)
}
}
這個中介軟體發起的action不會經過中介軟體呼叫鏈傳遞,而是直接使用store.dispatch()方法發起,原因很簡單:
// 上面的中介軟體發生錯誤的原因是因在初始化chain的時候發起了action
// 此時的dispatch 還沒有形成中介軟體呼叫鏈條,因而不能經過其他中介軟體而直接發起action
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 此時才會形成中介軟體呼叫鏈
dispatch = compose(...chain)(store.dispatch)
為了避免action不經過中介軟體傳遞的錯誤,redux修改了dispatch,也就是我們現在看到的樣子:
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)
}
這樣子,在中介軟體呼叫鏈未初始化完成之前,呼叫dispatch就會報錯誤提示。
redux-logger
最後我們再來看一個常用的中介軟體,redux-logger,其作用是輸出action發起前和發起後,state樹的值。
// 原始碼過於複雜
// 這裡直接簡化成如下形式:
let logger = ({ dispatch, getState }) => next => action => {
console.log('next:之前state', getState())
let result = next(action)
console.log('next:之前state', getState())
return result
}
redux-logger 的任務是輸出action發起前和發起後,state樹的值。它不關心action經過中介軟體鏈條時,發生了什麼變化,它只關注結果,這也是為什麼它的順序要放在最後面的原因,例如,我們在實際開發中常常這樣使用中介軟體:
const store = createStore(
reducer,
applyMiddleware([redux_thunk,promise,redux_logger])
)
action在傳遞中介軟體的過程中,直到最後一箇中間件(上文也就是redux_logger)時,才會應用原生的dispatch方法發起,此時才能看到state在action前後的變化,這也就是redux-logger為什麼要放在後面的原因了。
最後
Redux原始碼解讀就到此結束了,總體來說,整個框架的設計非常的簡潔易讀,不禁感嘆作者之強了,將flux思想轉換為redux思想,最終寫出一個易用的框架。在實際開發中,通常不直接使用redux作為react的擴充套件,而是會使用react-redux這個庫,正如redux所言,redux並非為react而生,react-redux充當了相容二者的關係,但其本源是通過react的context,提供一個全域性的provider元件包裹整個app應用,將redux定義在provider上,再通過封裝context獲取資料,更新資料的邏輯來使得在react任一個元件中都能非常簡潔地使用redux。
其實我們也可以看到,react-redux實際上是基於react的context設計的,是否有這樣一天,react的context會變得更為強大簡潔,使得在react就可以應用redux思想,實現狀態管理,而不依賴其他的第三庫呢?:)