1. 程式人生 > >Redux原始碼解讀 [05] 之 applyMiddleware和compose

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思想,實現狀態管理,而不依賴其他的第三庫呢?:)