前情提要
ps: 因為餘下的原始碼皆是短篇幅,於是一併合為一文進行書寫,如有不足之處望各位指正。
bindActionCreators
什麼是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
我們的bindActionCreators
與bindActionCreator
入參的差別就在於第一個引數,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雖然簡單,但是實現的功能確是很強大的,其像洋蔥一樣,一層一層的向外呼叫,每一個函式的結果都作為上外層函式的入參被呼叫,直到最後得出結果。
applyMiddleware
在說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
的原始碼中,學到了很多關於函數語言程式設計的思路和一些技巧,以及對各種邊界情況的重視。以前並不注重其中實現,覺得讓自己來也能寫出來,但是真到了自己看的時候才發現,原來還是圖樣圖森破,也只有感嘆一句:任重道遠啊…
最後,感謝你的閱讀~