Redux的action和reducer已經足夠複雜了,如今還須要理解Redux的中間件。爲何Redux的存在有何意義?爲何Redux的中間件有這麼多層的函數返回?Redux的中間件到底是如何工做的?本文來給你解惑,Redux中間件從零到「放棄」。git
本文的參考網站只有二個,首當其衝的就是Redux的官方網站,本文的思考過程大多參考官方給出的例子。還有一個就是Redux的經典中間件,能夠說Redux的中間件的產生就是爲了實現它——redux-thunk。es6
寫在前面:本文其實就是我理解Redux中間件的一個思考過程,中間難免來自我我的的吐槽,你們看看樂樂就好。github
咱們爲何要用中間件?這個問題提的好!爲了回答這個問題,我如今提出一個需求,全部的store.dispatch
都要監控dispatch以前和以後的state
變化。那麼咱們會怎作呢?So easy,直接先後都加上console.log(store.getState())
就能夠了不是嗎?redux
console.log('dispatching', action)
store.dispatch(getTodos({items:[]}))
console.log('next state', store.getState())
console.log('dispatching', action)
store.dispatch(getTodos({items:["aaa"]}))
console.log('next state', store.getState())
複製代碼
沒錯,咱們能夠這麼作。不過若是誇張點,我有成千上萬的dispatch,那麼console.log就要dispatch的數量*2了。而後當咱們倖幸苦苦打完點,產品要上線了,咱們須要把斷點都關閉。這個時候難道咱們要一個個去註釋刪除嗎?數組
不,我不幹,這樣可能還會改錯。那麼咱們將此功能獨立出來試試,這樣不就能夠實現複用了。將公用代碼寫入一個方法,而後變化的參數提取出來。promise
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
dispatchAndLog(store, getTodos({items:[]}))
dispatchAndLog(store, getTodos({items:["aaa"]}))
複製代碼
這樣是否是就方便了不少,註釋的話只須要註釋兩行,而不是隨着dispatch成倍數增加。可是我以爲這樣寫,對於其餘合做的小夥伴不友好,至關於我本身寫了一套語法出來。最好仍是使用官方的store.dispatch
的時候,自定義函數一塊兒執行了。bash
能夠這樣改寫store.dispatch
,將store.dispatch
賦值給next
,而後將diapatch
變成咱們自定義的函數,在這個自定義的函數中調用next
,也就是原dispatch
。這樣就玩美地改寫了dispatch
,保留了原始功能,還添加了自定義的方法。app
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
複製代碼
鏘鏘鏘~~~這個時候Redux中間件的雛形就出現了。異步
MiddleWare就是對dispatch方法的一個改造,一個變異。函數
那麼假象一下,我不只須要監控state,我可能還有其餘的功能。並且與監控state的方法相互獨立。也就是我須要多箇中間件,那麼該如何實現呢?
咱們能夠將每次的變異store.dispatch
都傳遞給一個新的參數,傳入下一次變異之中執行,可是要像這樣next1
,next2
……這樣源源不斷地下去嗎?
const next = store.dispatch
const next1 = store.dispatch = function dispatchAndLog1(action) {
console.log('dispatching', action)
let result = next(action)
console.log(result,'next state', store.getState())
return result
}
const next2 = store.dispatch = function dispatchAndLog2(action) {
console.log('dispatching1', action)
let result = next1(action)
console.log(result,'next state1', store.getState())
return result
}
...
...
...
複製代碼
這樣是否是格式有點醜?讓咱們想辦法解放next
參數。個人想法是這樣的,先寫一個compose,用來結合這些方法,而後返回一個變異的dispatch
方法。
const _dispatch=store.dispatch;
function compose(){
return function(action){
_dispatch(action)
}
}
store.dispatch=compose(dispatchAndLog1,dispatchAndLog2)
複製代碼
在實現compose方法以前咱們先考慮一個問題,如今middlewares的結構是這樣的,多層嵌套,一個函數嵌入一個函數,咱們改如何將這個方法從嵌套中解放出來呢?
function A(){
function B(){
function C(){
}
}
}
複製代碼
如何能避免面多層的嵌套?經過把函數賦值給一個參數,能夠解放嵌套,但這樣不太現實,由於咱們須要建立許多的參數。
const CM=function C(){}
const BM=function B(){
CM()
}
const AM=function A(){
BM()
}
複製代碼
爲了不建立許多沒必要要的引用,咱們能夠用傳遞參數的方式來解決這個問題,直接將函數看成參數傳入,那麼就要注意一個問題,由於咱們要先傳入函數,可是不執行各函數,因此每一個函數咱們都要返回一個函數,也就是建立高階函數,等都準備好了,從最外層的函數開始調用執行。
function C(){
return function(){}
}
function B(CM){
return function(){
CM()
}
}
function A(BM){
return function(){
BM()
}
}
複製代碼
這個方法執行的方式就很噁心,是一個函數嵌套後面的一個函數,將C返回的函數傳入B,而後將B返回的函數傳入A,最後執行()
,逐層執行函數,這樣也就沒有逃離回調地獄。
let compose=A(B(C()))
compose()
複製代碼
Array.reduce
登場這個時候咱們能夠考慮下Array.reduce
這個方法,將這些函數都合併起來。首先先建立一個數組,每一個函數傳遞一個next
的函數,以便於逐層執行函數。
let array=Array(3)
array[0]=function(next){
return function(){
let res= next();
return res
}
}
array[1]=function(next){
return function(){
let res= next();
return res
}
}
array[2]=function(next){
return function(){
let res= next();
return res
}
}
複製代碼
reduce只是合併,並非執行,你們注意了,因此咱們須要在每次執行以前加一層返回函數的操做。注意返回的函數須要和自定義函數的格式一致,也就是返回的函數須要傳參next
,至關於prevFunction
是以前兩個函數的結合,只有按照自定義函數的格式prevFunction
纔會有效。否則只有數組第一第二個會執行,由於初始值就是他們倆執行的結果返回。
function dispatch(){
console.log("dispatch")
return "dispatch"
}
function compose(array){
return array.reduce((prevFunction,currentFunction)=>{
return function (next) {
return prevFunction(currentFunction(next))
}
})
}
console.log(compose(array)(dispatch)());
複製代碼
這裏我定義了一個dispatch
做爲個人最初的next參數,傳入中間件的集合之中,最早推入棧的函數,是最後執行的,因次咱們的dispatch
會在最後一層函數執行。細心如大家應該發現了。個人每一個自定義函數都返回了上方next
的返回值。其實就是爲了將dispatch
的值返回。這樣compose
函數執行以後所獲得的值就是dispatch
的值。這樣咱們就能夠獲取原版store.dispatch
的值了。順便科普下原版store.dispatch
返回的值就是傳入action
。
根據上述思路,咱們來寫下合併中間件的compose
函數,首先將store.dispatch
給_dispatch
備用,而後compose這個高階函數的第一層參數是中間件,第二層就是初始next函數,也就是原版的store.dispatch
,咱們傳入副本_dispatch
就能夠了。最後改造store.dispatch
。
const _dispatch=store.dispatch;
function compose(){
let middlewares=Array(arguments.length).join(",").split(",")
middlewares=middlewares.map((i,index)=>{
return arguments[index];
})
return middlewares.reduce((prevFunction,currentFunction)=>{
return function (next) {
return prevFunction(currentFunction(next))
}
})
}
store.dispatch=compose(dispatchAndLog1,dispatchAndLog2)(_dispatch)
複製代碼
這樣咱們就能夠調用多箇中間件啦。
createStore
可是,官方的中間件可不是這麼些的。我翻譯了下官方對於應用中間件函數applyMiddleware()
的一個定義,其實就是對createStore
的一個加強enhance,也就是封裝啦。可是有如下幾點須要注意下:
createStore
的dispatch(action)
和getState()
方法。store.dispatch(action)
執行時,中間件的鏈也會執行,也就是綁定的中間件都要執行。createStore
,而不是createStore
返回的對象store
。也就是說在store
建立的時候,中間件已經執行完畢了。applyMiddleware()
要返回一個createStore
,也就是通過改造以後的createStore
那咱們就根據以上的注意點,理解下官方設定的applyMiddleware()
。 首先是如何加強 createStore
,同時有保證原有功能?
applyMiddleware()
要返回一個createStore
,也就是通過改造以後的createStore
function applyMiddlewareTest(){
return (createStore)=>{
return function (reducer) {
return createStore(reducer)
}
}
}
複製代碼
這樣調用applyMiddlewareTest()(createStore)(reducer)
不就等同於createStore(reducer)
。
store.dispatch(action)
執行時,中間件的鏈也會執行,也就是綁定的中間件都要執行。由於咱們不會控制中間件的數量applyMiddlewareTest(m1,m2,m3……)
,因此咱們採用arguments
的特性,來獲取中間件的數組,處理一下以後,調用咱們已經寫好的compose函合併一下,傳給_dispatch
,最後利用Object.assign
拷貝store
以及變異的dispatch
。
function applyMiddlewareTest(){
let middlewares=Array(arguments.length).join(",").split(",")
middlewares=middlewares.map((i,index)=>{
return arguments[index];
})
return (createStore)=>{
return function (reducer) {
let store = createStore(reducer)
let _dispatch=compose(middlewares)(store.dispatch)
return Object.assign({},store,{
dispatch:_dispatch
})
}
}
}
複製代碼
createStore
的dispatch(action)
和getState()
方法。咱們如今寫的中間件是沒法從函數內部中獲取到dispatch(action)
和getState()
,因此咱們須要多寫一層函數,傳入dispatch(action)
和getState()
。爲了簡潔,咱們能夠傳入一個對象,包含了入dispatch(action)
和getState()
兩個方法
function dispatchAndLog2({dispatch,getState}){
return function (next){
return function (action) {
console.log('dispatching1', action)
let result = next1(action)
console.log(result,'next state1', store.getState())
return result
}
}
}
複製代碼
這個函數能夠簡化爲es6的寫法:
const dispatchAndLog2=({dispatch,getState})=>next=>action{
....
}
複製代碼
出現了!三層函數啊,第一層爲了傳遞store的dispatch(action)
和getState()
方法,第二層傳遞的參數next
是下一個待執行的中間件,第三層是函數本體了,傳遞的參數action
是爲了最終傳遞給dispatch
而存在的。
回到applyMiddlewareTest
,中間件中須要的dispatch
和getState
,咱們能夠加幾行代碼實現。直接執行中間件的第一層,將兩個方法傳遞進去。此處須要注意dispatch
由於咱們須要傳遞的dispatch
是變異以後的,而不是原生的。因此邊咱們改寫下dispatch
的方法,讓中間件調用此方法時,是變異後的dispatch
。否則中間件中執行的dispatch就沒法執行中間件了。
function applyMiddlewareTest(){
...
let _dispatch=store.dispatch
let _getState=store.getState
let chain = middlewares.map(function (middleware) {
return middleware({
dispatch:function dispatch() {
return _dispatch.apply(undefined, arguments);
},
getState:_getState
});
});
_dispatch=compose(chain)(store.dispatch)
....
}
複製代碼
最後測試一波本身寫的中間件是否成功:
function logger({ getState }) {
return function(next){
return function(action){
console.log('will dispatch', action)
const returnValue = next(action)
console.log('state after dispatch', getState())
return returnValue
}
}
}
const ifActionIsFunction = {dispatch,getState} => next => action => {
if (typeof action === 'function') {//若是是函數就執行並返回,而後再函數中執行dispatch,至關於延遲了dispatch。
return action(dispatch, getState);
}else{
let res=next(action)
return res
}
}
let store=applyMiddlewareTest(logger,ifActionIsFunction)(createStore)(rootReducer)
store.dispatch((dispatch,getState)=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
dispatch(getTodos({items:["aaaa"]}))
console.log(getState())
resolve()
},1000);
})
})
複製代碼
運行是成功的,這裏我寫的中間件的功能是是若是action
是函數,那麼就返回函數的執行結果,而且向函數中傳入dispatch
和getState
方法。這樣就能夠在action
函數中調用dispatch了。機智如你必定發現了這個就是異步的一個實現,也就是redux-thunk
的基本邏輯。(其實就是參照redux-thunk
寫的。)
這裏還有一個隱藏功能不知道你們發現了沒有,我返回的是一個promise,也就是說我能夠實現then
的鏈式調用。
store.dispatch((dispatch,getState)=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
dispatch(getTodos({items:["aaaa"]}))
console.log(getState())
resolve("then方法調用成功了嗎?")
},1000);
})
}).then((data)=>{
console.log(data)
})
複製代碼