逐行閱讀redux源碼(三)bindActionCreators & applyMiddleware & compose

前情提要

ps: 由於餘下的源碼皆是短篇幅,因而一併合爲一文進行書寫,若有不足之處望各位指正。html

bindActionCreators

image

什麼是actionCreators

相信你們在閱讀了以前的文章以後,對什麼是action已經很熟悉了,咱們的action其實就是一個object,一般其中會包含一個type參數和一個數據載體payload參數,用來給redux在使用dispatch更新狀態樹時指一條明路。react

那麼什麼是actionCreators呢?git

那麼從字面而言,actionCreators能夠理解爲:動做建立器,也就是說咱們的actionCreators是用來生成action(動做)的。咱們在業務開發的時候,一般會使用不少不一樣的action,可是這些action不少時候都會包括一個不定變量,好比咱們在更新一個列表的時候,咱們可能會使用這樣一個action:編程

async function getListData() {...}
await const listData = getListData();

dispatch({
    type: 'updateList',
    data: listData
});
複製代碼

雖然寫一個匿名的action也ok,可是在大型項目這樣寫的話維護將存在極大問題的,你根本就不知道這個action有什麼做用(大部分狀況下),因此咱們在統一管理這類action時一般使用actionCreators(固然也爲了讓action變得更加靈活):redux

// actionCreator一般爲純函數
function updateList(listData) {
    return {
        type: 'updateList',
        data: listData
    }
}
複製代碼

爲何須要bindActionCreators

咱們在書寫頁面組件的時候,若是要用react-redux更新咱們的狀態樹而後重渲染頁面,一般會使用redux綁定在頁面的props上的dispatch方法觸發action數組

當咱們的頁面上不存在子組件,或者子組件中不須要使用redux時這樣作是沒有任何問題的,可是當咱們須要在子組件中使用dispatch時,咱們就會發現,子組件的Props是純淨的,沒有任何地方能夠調用dispatchbash

固然你也能夠把父組件中的dispatch方法經過props傳遞到子組件中使用,可是正確的操做,仍是使用咱們的bindActionCreatorsapp

咱們能夠將咱們須要使用的actionCreators封裝至一個對象中,key值就是actionCreators的函數名,值就是對應其函數:async

function action1(param){...}
function action2(param){...}

const actionCreators = {
    action1,
    action2
}
複製代碼

而後咱們使用bindActionCreators:ide

const { dispatch } = this.props;

const dispatchActionsObject = bindActionCreators(actionCreators, dispatch);
//dispatchActionsObject爲:
{
    action1: (param) => {dispatch(action1(param))},
    action2: (param) => {dispatch(action2(param))}
}
複製代碼

咱們只須要將dispatchActionsObject經過props綁定到子組件上,那麼子組件就能夠在不知道是否存在redux的狀況下使用咱們提供的函數來更新狀態樹了。

// 父組件
...
class Parent extends React.Component {
    ...
    render(
        <Parent>
            <Child {...dispatchActionsObject}></Child>
        </Parent>
    )
}


// 子組件
class Child extends React.Component {
  ...
  doAction1(data) {
      this.props.action1(data)
  },
  doAction2(data) {
      this.props.action2(data)
  }
}

複製代碼

源碼

bindActionCreator
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
複製代碼

bindActionCreators的開始部分,咱們會發現這個方法bindActionCreator接受了兩個參數,一個actionCreator,一個dispatch

隨後其做爲高階函數,返回了一個新的匿名函數,並在匿名函數中執行了dispatch操做,使用Function.prototype.apply,接受了外部的傳參值,並傳入actionCreator中生成action

因此這個函數的功能就是:將傳入的單個actionCreator封裝成dispatch這個actionCreator的函數。 例如:

const actionCreator(data) {
    return {
        type: 'create',
        data
    }
};

const dispatch = this.props.dispatch;

const dispatchActionCreatorFn = bindActionCreator(actionCreator, dispatch);
// 這個方法會返回一個function
// actionObj = (data) => {
//    dispatch(actionCreator(data);
// }
複製代碼
bindActionCreators

咱們的bindActionCreatorsbindActionCreator入參的差異就在於第一個參數,bindActionCreator接受的是一個function,而bindActionCreators則接受的是function或者object

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }
  ...
}  
複製代碼

從源碼上看,咱們的bindActionCreators作了一些簡單的防錯和兼容處理,當接受的是function時,其做用和直接bindActionCreator是一致的,都是返回一個隱式調用dispatch的方法,而當其傳入的是一個object時,會強制阻止用戶使用null爲參數傳入。

const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
  boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
複製代碼

在防錯處理以後,即是咱們整個bindActionCreators的核心部分。咱們會經過Object.keys()遍歷獲取actionCreators對象的key值數組,並聲明一個空對象boundActionCreators準備用來存放咱們即將生成的隱式調用dispatch的方法。

其後咱們經過遍歷key值數組,將每一個key值對應的actionCreator取出,簡單的判斷actionCreator是否合規,而後使用bindActionCreator生成對應的匿名函數,並存放在咱們以前聲明的boundActionCreators的同key值之下,這以後,咱們的bindActionCreators完成了~

compose

由於applyMiddleware依賴於函數式編程的compose(組合),咱們先從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)))
}
複製代碼

compose是個很是簡單的純函數,其做用是經過reduce將傳入的方法按照從右到左的順序組合起來。

實現過程讓咱們一步一步來嘗試:

ps: 但願你在閱讀compose源碼以前先了解rest箭頭函數

  • 首先是一些兼容性處理,避免函數報錯
  • 使用reduce遍歷數組中的function,咱們能夠用簡單的例子來看看到底發生了什麼
function addOne(x) {
    return x+1;
}

function double(x) {
    return x*2;
}

function println(x) {
    retrun x;
}

const funcs = [println, double, addOne];
複製代碼

當咱們第一次reduce的時候:

// 由於不存在 `initialValue` 
// 因此第一次reduce的第一個參數能夠視做`currentValue`
// 第二個參數視做`nextValue`

[println, double, addOne].reduce((a,b) => {
    return (...args) => {a(b(...args))}
})

// 得出的結果是
[println, double, addOne].reduce((a,b) => {
    return (...args) => {print(double(...args))}
})
複製代碼

此時咱們的reduce能夠看做是:

[ (...args) => {print(double(...args))}, addOne].reduce((a,b) => {...})
複製代碼

由於此時reduce回調中的第一個參數會變成上一次遍歷時的返回值,因此接下來的reduce會變成這樣:

[ (...args) => {print(double(...args))}, addOne].reduce((a,b) => {
    // 此時 a = (...args) => {print(double(...args));
    // b = addOne
    // 因此 a(b(..args)) = print(double(addOne(...args)))
    return (...args) => {
        print(double(addOne(..args)))
    }
})
複製代碼

由此能夠看出,咱們的compose雖然簡單,可是實現的功能確是很強大的,其像洋蔥同樣,一層一層的向外調用,每個函數的結果都做爲上外層函數的入參被調用,直到最後得出結果。

image

applyMiddleware

image

在說applyMiddlewares以前,讓咱們先回憶一下咱們的createStore中傳入了enhancer後的處理過程:

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  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)
  }
複製代碼

能夠看到,當createStore接收到可用的enhancer時,會將creteStore做爲參數傳入高階函數enhancer中,並使用以前傳入的reducer/preloadedState繼續進行建立store.

而實現enhancer的方法,就是applyMiddleware.

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
    }
  }
}
複製代碼

能夠從源碼看到,咱們的applyMiddleware返回的即是一個柯里化的高階函數,其接受入參的順序也符合enhancer在被調用時的順序,因此咱們不妨直接把...args直接替換成reducer, preloadedState.此時,咱們的createStore會變成這樣:

const store = createStore(reducer, preloadedState)
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,阻止用戶在沒有處理完middleware時調用真正的dispatch影響狀態樹致使中間件middleware不能獲取實時的狀態值。

接下來,其建立了一個middlewareAPI,用來存放當前store的狀態值和以前會拋錯的dispatch,並做爲參數,結合middlewares生成一組匿名函數,每一個匿名函數的返回值都是經過當前middleware處理了middlewareAPI的返回值,以後經過compose,將這些匿名函數組合,讓其可以從右到左的鏈式調用,最後再爲組合生成的函數傳入store.dispatch(真正的disaptch)做爲參數值,其過程以下:

// 假設我有兩個middleware: mdw1, mdw2
// compose過程以下:
// 而mdw1, mdw2通過遍歷是以下的數組
[mdw1(middlewareAPI), mdw2(middlwwareAPI)]
// 通過compose後
return (...args) => mdw1(middlewareAPI)(mdw2(middlwwareAPI)(...args))

// 傳入store.dispatch以後
return mdw1(middlewareAPI)(mdw2(middlwwareAPI)(store.dispatch))
複製代碼

這樣處理以後,咱們經過鏈式調用中間件處理完成的dispatch才能正式return出去,做爲store的一部分。

結語

本文至此,redux的5個文件的源碼也算是告一段落,從閱讀redux的源碼中,學到了不少關於函數式編程的思路和一些技巧,以及對各類邊界狀況的重視。之前並不注重其中實現,以爲讓本身來也能寫出來,可是真到了本身看的時候才發現,原來仍是圖樣圖森破,也只有感嘆一句:任重道遠啊...

最後,感謝你的閱讀~

相關文章
相關標籤/搜索