1. 程式人生 > >redux的中介軟體applyMiddleware原始碼詳解

redux的中介軟體applyMiddleware原始碼詳解

原文: redux的applyMiddleware原始碼 – 歡迎 watch || star

記得之前第一次看redux原始碼的時候是很懵逼的,尤其是看到applyMiddleware函式的時候,更是懵逼。當然那也是半年前的事情了,前幾天把redux原始碼看了下,並且實現了個簡單的redux功能。但是沒有實現中介軟體。今天突然又想看看redux的中介軟體,就看了起來。

記得半年之前是從函式宣告的下一行就開始看不懂了。。。然後前段時間,看了下柯里化函式,加深了高階函式的印象,所以今天打算把中介軟體的原始碼給擼一下。

我們來看看函式宣告的下一行,也就是原始碼第二行開始看:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    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 }
} }

從上面我們可以看到中介軟體返回了函式,返回第一個函式是攜帶createStore引數的,這個是啥?從名字上我們就可以知道,就是createStore。不過為了證明,我們還是得從原始碼上來看。

還記得是怎麼呼叫的中介軟體的吧,大致如下:

const store = createStore(
    reducer,
    applyMiddleware(...middlewares)
);

可以看到中介軟體是在createStore引數裡呼叫的(在引數裡執行函式,導致傳遞給createStore的是中介軟體執行後返回的結果,從上面的中介軟體原始碼可以知道,返回的就是攜帶createStore引數的函式),現在我們可以createStore函式裡看看他是怎麼處理中介軟體返回的函式的。

redux的主要實現都是在createStore裡實現的,所以我們主要看createStore裡處理引數的部分:

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  //other code
}

從我們呼叫createStore可以知道第一個引數是reducer,第二個引數就是中介軟體執行之後返回的攜帶createStore引數的函式。但是在上面的這段原始碼裡,我們發現是preloadedState來接收這個攜帶createStore引數的函式,感覺不是很多,命名的’不好’。先繼續往下看,wow, 是一個判斷,他會判斷preloadedState是不是一個函式,第三個引數enhancer是不是未定義;如果preloadedState是函式,enhancer是未定義,那麼就會把preloadedState賦值給enhancer,並且設定preloadedState是未定義。 這樣就沒有問題了,在這裡,相當於第三個引數enhancer接收了攜帶createStore引數的函式。

然後第二個判斷:

if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }

  return enhancer(createStore)(reducer, preloadedState)
}

他會去執行這個enhancer。這個enhancer是什麼?就是我們說的攜帶createStore的函式。

有意思的是,這個enhancer直接在這裡運行了,並且採用了createStore作為引數(這個createStore就是函式呀)。 我們再來看看enhancer(createStore)返回的是啥:

return function (...args){
      const store = createStore(...args)
      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
      }
    }

有意思,返回的是帶有多個引數的函式。
上面的程式碼相當於:

enhancer(createStore) ~= function(...args) => function(reducer, preloadedState)

可以看到,上面的(...args)就是相當於(reducer, preloadedState)

那麼我們再來看看上面的function(...args), 額, 直接在第一行就再次呼叫建立store,這樣不會陷入無限迴圈嗎?不會,因為有引數判斷,在createStore的原方法裡不會再執行enhancer; 所以我們可以發現,在有中介軟體的時候,真正的執行createStore是在中介軟體裡去執行的,並且攜帶的引數是reducer, preloadedState

所以上面第一行建立了個store物件,他返回的屬性有:

{
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

然後新建了個指向dispatch變數的匿名函式,這個函式在呼叫的時候丟擲異常告訴你不可以在構造中介軟體時呼叫dispatch

Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.

接下來會建立一個middlewareAPI物件:

const middlewareAPI = {
    getState: store.getState, //獲取store裡的state
    dispatch: (...args) => dispatch(...args) // 呼叫`dispatch`的時候會拋錯,如果在組合中介軟體之前呼叫,下面會說
}

一開始我以為是在呼叫的時候就會報錯,可是發現這個物件裡的dispatch攜帶引數,如果只是單純拋錯,完全可以不需要傳遞引數,然後向下看下去才看到其中的奧妙。

然後就是對中介軟體集合middlewares(陣列)進行操作:

const chain = middlewares.map(middleware => middleware(middlewareAPI)) //返回了新的集合,對應的每個中介軟體呼叫的結果

然後就是組合這些中介軟體了, 這裡對高階函式不熟的,可以看下柯里化函式和函式組合::

// china是上面返回的中介軟體的結果
dispatch = compose(...chain)(store.dispatch)

可以看到這個程式碼,組合了中介軟體, 使用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)))
}

上面的程式碼比較有意思:

  • 第一個判斷
    如果沒有中介軟體作為引數傳遞,那麼直接返回一個函式,接收對應的引數並且返回這個引數。
  • 第二個判斷
    如果如果這個中介軟體引數只有一個,那麼直接返回這個中介軟體函式
  • 最後一步
    那就是多箇中間件傳遞進來的時候,他會借用reduce方法組合(這個放在後面), 會有個...args引數,就是(store.dispatch),等下回說到。
    可能你對reduce不是很熟,可以簡單的看下他幹了什麼事:
['1', 2, 3, 'n'].reduce((a, b) => console.log('a is',a , 'b is', b)) // 這樣你就會發現這個方法在這裡的作用

其實從註釋裡也可以知道:

* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).

把這些中介軟體都執行到dispatch.

再回到上面看compose的返回:

return funcs.reduce((a, b) => (...args) => a(b(...args)))

我們再看看中介軟體呼叫compose的地方:

dispatch = compose(...chain)(store.dispatch)

從這個地方再配合看compose(...chain) => result的這個result.

  • 第一個判斷
    返回的是(arg) => arg就是相當於 result(arg) => arg, 果然,直接返回這個store.dispatch
  • 第二個判斷
    返回的是唯一的一箇中間件result. 然後中介軟體直接呼叫store.dispatch作為引數。
  • 最後一個
    這個返回的是一個函式,看起來像這樣:
(...args) => a(b(...args))

這樣就相當於result(args) => a(b(...args)),這樣就保證每個中介軟體都會用到dispatch,並且最終返回這個被擴充套件過的dispatch.

然後可以看到中介軟體函式返回了物件:

{
  ...store,
  dispatch
}

這個dispatch就是被處理過的dispatch