隨筆前言
在上一周的學習中,我們熟悉了如何通過redux去管理數據,而在這一節中,我們將一起深入到redux的知識中學習。
首先談一談為什么要用到middleware
我們知道在一個簡單的數據流場景中,點擊一個button后,在回調中分發一個action,reducer收到action后就會更新state並通知view重新渲染,如下圖所示
但是如果需要打印每一個action來調試,就得去改dispatch或者reducer實現,使其具備打印功能,那么該如何做?因此,需要中間件的加入。
上圖展示了應用middleware后的Redux處理事件的邏輯,每個middleware都可以處理一個相對獨立的事物,通過串聯不同的middleware實現變化多樣的功能!
小結:Redux中的reducer更加的專注於轉化邏輯,所以middleware是為了增強dispatch而出現的。
middleware是如何工作的
Redux提供了一個applyMiddleware方法來加載middleware,它的源碼是這樣的:
import compose from './compose';
export default function applyMiddleware(...middlewares) {
return (next) => (reducer, initalState) => {
let store = next(reducer, initalState);
let dispatch = store.dispatch;
let chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map( middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
}
}
然后我們再上一個logger middleware的源碼實現:
export default store => next => action => {
console.log('dispatch:', action);
next(action);
console.log('finish:', action);
}
雖然看到“源碼”的那兩個字的時候,內心一萬只草什么馬奔過,但是一看到代碼這么精簡,這么優美,那就初讀一下源碼把。
然后
接下來就開始解讀上面源碼
深入解析middleware運行原理
1. 函數式編程思想設計
middleware是一個層層包裹的匿名函數,這其實是函數式編程的currying(Currying就是把一個帶有多個參數的函數拆分成一系列帶部分參數的函數)。那么applyMiddleware會對logger這個middleware進行層層的調用,動態的將store和next參數賦值。
那么currying的middleware結構有什么好處呢?
- 1.1 易串聯: currying函數具有延遲執行的特性,通過不斷currying形成的middleware可以積累參數,再配合組合(compose)的方式,這樣很容易就形成pipeline來處理數據流
- 1.2 共享store:在applyMiddleware執行的過程當中,store還是舊的,但是因為閉包的存在,applyMiddleware完成之后,所有的middleware內部拿到的store是最新的且是相同的。
並且applyMiddleware的結構也是一個多層currying的函數,借助compose,applyMiddleware可以用來和其他插件加強createStore函數
2. 給middleware分發store
通過如下方式創建一個普通的store
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
上述代碼執行完后,applyMiddleware方法陸續獲得了3個參數,第一個是middlewares數組[mid1, mid2, mid3,...],第二個是Redux原生的createStore方法,最后一個是reducer。然后我們可以看到applyMiddleware利用createStore和reducer創建了一個store。而store的getState方法和dispatch方法又分別被直接和間接地賦值給middlewareAPI變量的store
const middleAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middle => middleware(middlewareAPI))
然后,每個middleware帶着middlewareAPI這個參數分別執行一遍,執行后,得到一個chain數組[f1, f2, ..., fx, ..., fn],它保存的對象是第二個箭頭函數返回的匿名函數。因為是閉包,每個匿名函數多可以訪問相同的store,即middlewareAPI.
3.組合串聯middleware
這一層只有一行代碼,確是applyMiddleware精華所在。
dispatch = compose(...chain)(store.dispatch);
其中,compose是函數式編程中的組合,它將chain中的所有匿名函數[f1, f2, ..., fn]組裝成一個新的函數,即新的dispatch。當新的dispatch執行的時候,[f1, f2, ...]會從右到左依次執行。Redux中compose的實現是這樣的,當然實現的方式不唯一。
function compose(...funs) {
return arg => funcs.reduceRight( (compose, f) => f(composed), arg)
}
compose(...funcs)返回的是一個匿名函數,其中funcs就是chain數組。當調用reduceRight時,依次從funcs數組的右端取一個函數f(x)拿來執行,f(x)的參數composed就是前一次f(x+1)執行的結果,而第一次執行的f(n)n代表chain的長度,它的參數arg就是store.dispatch。
因此,當compose執行完后,我們得到的dispatch是這樣的:
假設n=3:
dispatch = f1(f2(f3(store.dispatch)));
這時調用dispatch,每一個middleware就會依次執行了。
4.在middleware中調用dispatch會發生什么呢?
經過compose后,所有的middleware就算是已經串聯起來了。
那么問題來了?
在分發store時,我們有說到每個middleware都可以訪問store,也就是我們說的通過middlewareAPI這個變量去拿到dispatch屬性進行操作。那么如果在middleware中調用store.dispatch會如何?和調用next()有什么區別?
先上一波代碼:
//next()
const logger = store => next => action => {
console.log('dispatch:', action);
next(action);
console.log('finish:', action );
}
//store.dispatch(action);
const logger = store => next => action {
console.log('dispatch:', action);
store.dispatch(action);
console.log('finishL:', action);
}
在分發store的時候,我們有說過:midddleware中的store的dispatch通過匿名函數的方式和最終compose結束后的新dispatch保持一致,所以,在middleware中調用store.dispatch()和在其他任何地方調用其實效果是一樣的。
而如果在middleware中調用next(),效果是進入下一個middleware中。
具體如下兩個圖1和圖2所示:
如圖1所示,正常情況下,當我們去分發一個action時,middleware會通過next(action)一層層處理和傳遞action直到redux原生的dispatch。如果某個middleware中使用了store.dispatch(action)來分發action,就會發生如圖2所示的情況。這就相當於是又從頭開始了。
那么問題又來了,假如這個middleware一直簡單粗暴地調用store.dispatch(action),就會形成一個無限循環了,那么store.dispatch(action)的用武之地到底在哪里呢?
假如我們需要發送一個異步請求到服務端獲取數據,成功后彈出個message,這里我們通常會用到reduce-thunk這個插件。
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
代碼很清晰,就是會先判斷你dispatch過來的action是不是一個function,如果是則執行action,如果不是則傳遞到下一個middleware。因此,我們可以這樣設計action:
const getMessage = (dispatch, getState) => {
const url = 'http://xxx.json';
Axios.get(url)
.then( res => {
dispatch({
type: 'SHOW_MESSAGE',
message: res.data.data
})
})
.catch( err => {
dispatch({
type: 'ERR_GET',
message: 'error'
})
})
}
如上所示,只要在應用中去調用store.dispatch(getThenShow), redux-thunk這個middleware接收到后就會去執行getMessage方法。getMessage會先請求數據,根據請求結果去做相對應的分發action。這這里的dispatch就是通過redux-thunk這個middleware傳遞進來的。
總結:在middleware中使用dispatch的場景一般是接受到一個定向action,而這個action又並不希望到達原生的分發action,往往用在異步請求的需求里面。
----------------作者的話:其實看了挺多遍,在腦海中構建整個middleware流程,再結合上周學習Redux時的Demo才漸漸的知其形,還需要多在實踐中會其神!--------------