Redux的中介軟體Middleware不難,我信了^_^

cherryvenus發表於2019-03-02

Redux的action和reducer已經足夠複雜了,現在還需要理解Redux的中介軟體。為什麼Redux的存在有何意義?為什麼Redux的中介軟體有這麼多層的函式返回?Redux的中介軟體究竟是如何工作的?本文來給你解惑,Redux中介軟體從零到“放棄”。

本文的參考網站只有二個,首當其衝的就是Redux的官方網站,本文的思考過程大多參考官方給出的例子。還有一個就是Redux的經典中介軟體,可以說Redux的中介軟體的產生就是為了實現它——redux-thunk

寫在前面:本文其實就是我理解Redux中介軟體的一個思考過程,中間不免來自我個人的吐槽,大家看看樂樂就好。

我們為什麼要用中介軟體?

我們為什麼要用中介軟體?這個問題提的好!為了回答這個問題,我現在提出一個需求,所有的store.dispatch都要監控dispatch之前和之後的state變化。那麼我們會怎做呢?So easy,直接前後都加上console.log(store.getState())就可以了不是嗎?

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了。然後當我們倖幸苦苦打完點,產品要上線了,我們需要把斷點都關閉。這個時候難道我們要一個個去註釋刪除嗎?

不,我不幹,這樣可能還會改錯。那麼我們將此功能獨立出來試試,這樣不就可以實現複用了。將公用程式碼寫入一個方法,然後變化的引數提取出來。

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的時候,自定義函式一起執行了。

可以這樣改寫store.dispatch,將store.dispatch賦值給next,然後將diapatch變成我們自定義的函式,在這個自定義的函式中呼叫next,也就是原dispatch。這樣就玩美地改寫了dispatch,保留了原始功能,還新增了自定義的方法。

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都傳遞給一個新的引數,傳入下一次變異之中執行,但是要像這樣next1next2……這樣源源不斷地下去嗎?

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)
複製程式碼

這樣我們就可以呼叫多箇中介軟體啦。

Redux的中介軟體Middleware不難,我信了^_^

融入createStore

但是,官方的中介軟體可不是這麼些的。我翻譯了下官方對於應用中介軟體函式applyMiddleware()的一個定義,其實就是對createStore的一個增強enhance,也就是封裝啦。但是有以下幾點需要注意下:

  • 自定義中介軟體可以獲取到createStoredispatch(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
            })
        }
    }
}
複製程式碼

自定義中介軟體可以獲取到createStoredispatch(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,中介軟體中需要的dispatchgetState,我們可以加幾行程式碼實現。直接執行中介軟體的第一層,將兩個方法傳遞進去。此處需要注意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)
    ....
}
複製程式碼

redux-thunk的實現

最後測試一波自己寫的中介軟體是否成功:

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是函式,那麼就返回函式的執行結果,並且向函式中傳入dispatchgetState方法。這樣就可以在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)
})
複製程式碼

相關文章