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

santree發表於2018-11-23

前情提要

ps: 因為餘下的原始碼皆是短篇幅,於是一併合為一文進行書寫,如有不足之處望各位指正。

bindActionCreators

image

什麼是actionCreators

相信大家在閱讀了之前的文章之後,對什麼是action已經很熟悉了,我們的action其實就是一個object,通常其中會包含一個type引數和一個資料載體payload引數,用來給redux在使用dispatch更新狀態樹時指一條明路。

那麼什麼是actionCreators呢?

那麼從字面而言,actionCreators可以理解為:動作建立器,也就是說我們的actionCreators是用來生成action(動作)的。我們在業務開發的時候,通常會使用很多不同的action,但是這些action很多時候都會包括一個不定變數,比如我們在更新一個列表的時候,我們可能會使用這樣一個action:

async function getListData() {...
}await const listData = getListData();
dispatch({
type: 'updateList', data: listData
});
複製程式碼

雖然寫一個匿名的action也ok,但是在大型專案這樣寫的話維護將存在極大問題的,你根本就不知道這個action有什麼作用(大部分情況下),所以我們在統一管理這類action時通常使用actionCreators(當然也為了讓action變得更加靈活):

// actionCreator通常為純函式function updateList(listData) { 
return {
type: 'updateList', data: listData
}
}複製程式碼

為什麼需要bindActionCreators

我們在書寫頁面元件的時候,如果要用react-redux更新我們的狀態樹然後重渲染頁面,通常會使用redux繫結在頁面的props上的dispatch方法觸發action

當我們的頁面上不存在子元件,或者子元件中不需要使用redux時這樣做是沒有任何問題的,但是當我們需要在子元件中使用dispatch時,我們就會發現,子元件的Props是純淨的,沒有任何地方可以呼叫dispatch

當然你也可以把父元件中的dispatch方法通過props傳遞到子元件中使用,但是正確的操作,還是使用我們的bindActionCreators

我們可以將我們需要使用的actionCreators封裝至一個物件中,key值就是actionCreators的函式名,值就是對應其函式:

function action1(param){...
}function action2(param){...
}const actionCreators = {
action1, action2
}複製程式碼

然後我們使用bindActionCreators:

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的原始碼中,學到了很多關於函數語言程式設計的思路和一些技巧,以及對各種邊界情況的重視。以前並不注重其中實現,覺得讓自己來也能寫出來,但是真到了自己看的時候才發現,原來還是圖樣圖森破,也只有感嘆一句:任重道遠啊…

最後,感謝你的閱讀~

來源:https://juejin.im/post/5bf7c297f265da616a474fc6

相關文章