用法
為了對中介軟體有一個整體的認識,先從用法開始分析。呼叫中介軟體的程式碼如下:
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === `function` && typeof enhancer === `undefined`) {
enhancer = preloadedState
preloadedState = undefined
}
}複製程式碼
enhancer
是中介軟體,且第二個引數為 Function
且沒有第三個引數時,可以轉移到第二個引數,那麼就有兩種方式設定中介軟體:
const store = createStore(reducer, null, applyMiddleware(...))複製程式碼
const store = createStore(reducer, applyMiddleware(...))複製程式碼
再看 原始碼 中介軟體的傳參:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
...
}複製程式碼
就是為了得到 store
,並通過 createStore
建立,上述兩種方法因為在 createStore
函式內部傳入了自身函式才得以實現 :
export default function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== `undefined`) {
return enhancer(createStore)(reducer, preloadedState)
}
...
}複製程式碼
上述程式碼可以看出,建立 store 的過程完全交給中介軟體了,因此開啟了中介軟體第三種使用方式:
const store = applyMiddleware(...)(createStore)複製程式碼
applyMiddleware 原始碼解析
大家對剖析 applyMiddleware
原始碼都非常感興趣,因為它實現精簡,但含義甚廣,再重溫其原始碼:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}複製程式碼
假設大家都已瞭解 ES6 7 語法,懂得 compose
函式的含義,並且看過一些原始碼剖析了,我們才能把重點放在核心原理上:為什麼中介軟體函式有三個傳參 store => next => action
,第二個引數 next
為什麼擁有神奇的作用?
store
程式碼前幾行建立了 store
(如果第三個引數是中介軟體,就會出現中介軟體 store 包中介軟體 store 的情況,但效果是完全 打平 的), middlewareAPI
這個變數,其實就是精簡的 store
, 因為它提供了 getState
獲取資料,dispatch
派發動作。
下一行,middlewares.map
將這個 store
作為引數執行了一遍中介軟體,所以中介軟體第一級引數 store
就是這麼來的。
next
下一步我們得到了 chain
, 倒推來看,其中每個中介軟體只有 next => action
兩級引數了。我們假設只有一箇中介軟體 fn
,因此 compose
的效果是:
dispatch = fn(store.dispatch)複製程式碼
那麼 next
引數也知道了,就是 store.dispatch
這個原始的 dispatch
.
action
程式碼的最後,返回了 dispatch
,我們一般會這麼用:
store.dispatch(action)複製程式碼
等價於
fn(store.dispatch)(action)複製程式碼
第三個引數也來了,它就是使用者自己傳的 action
.
單一中介軟體的場景
我們展開程式碼來檢視一箇中介軟體的執行情況:
fn(middlewareAPI)(store.dispatch)(action)複製程式碼
對應 fn
的程式碼可能是:
export default store => next => action => {
console.log(`beforeState`, store.getState())
next(action)
console.log(`nextState`, store.getState())
}複製程式碼
當我們執行了 next(action)
後,相當於呼叫了原始 store
dispatch
方法,並將 action
傳入其中,可想而知,下一行輸出的 state
已經是更新後的了。
但是 next
僅僅是 store.dispatch
, 為什麼叫做 next
我們現在還看不出來。
function dispatch(action) {
...
currentState = currentReducer(currentState, action)
...
}複製程式碼
其中還有一段更新監聽陣列物件,以達到 dispatch
過程不受干擾(快照效果) 作為課後作業大家獨立研究:主要思考這段程式碼的意圖:github.com/reactjs/red…
多中介軟體的場景
我們假設有三個中介軟體 fn1
fn2
fn3
, 從原始碼的這兩句入手:
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)複製程式碼
第一行程式碼,我們得到了只剩 next => action
引數的 chain
, 暫且叫做:
cfn1
cfn2
cfn3
, 並且有如下對應關係
cfnx = fnx(middlewareAPI)複製程式碼
第二行程式碼展開後是這樣的:
dispatch = cfn1(cfn2(cfn3(store.dispatch)))複製程式碼
可以看到最後傳入的中介軟體 fn3
最先執行。
為了便於後面理解,我先把上面程式碼的含義寫出來:通過傳入原始的 store.dispatch
, 希望通過層層中介軟體的呼叫,最後產生一個新的 dispatch
. 那麼實際上,中介軟體所組成的 dispatch
, 從函式角度看,就是被執行過一次的 cfn1
cfn2
cfn3
函式。
我們就算不理解新 dispatch
的含義,也可以從程式碼角度理解:只要執行了新的 dispatch
, 中介軟體函式 cfnx
系列就要被執行一次,所以 cfnx
的函式本身就是中介軟體的 dispatch
。
對應 cfn3
的程式碼可能是:
export default next => action => {
next(action)
}複製程式碼
這就是這個中介軟體的 dispatch
.
那麼執行了 cfn3
後,也就是 dispatch
了之後,其內部可能沒有返回值,我們叫做 ncfn3
,大概如下:
export default action => {}複製程式碼
但其函式自身就是返回值 返回給了 cfn2
作為第一個引數,替代了 cnf3
引數 store.dispatch
的位置。
我們再想想,store.dispatch
的返回值是什麼?不就是 action => {}
這樣的函式嗎?這樣,一箇中介軟體的 dispatch
傳遞完成了。我們理解了多中介軟體 compose
後可以為什麼可以組成一個新的 dispatch
了(其實單一中介軟體也一樣,但因為步驟只有一步,讓人會想到直接觸發 store.dispatch
上,多中介軟體提煉了這個行為,上升到組合為新的 dispatch
)。
再解釋 next 的含義
為什麼我們在中介軟體中執行 next(action)
,下一步就能拿到修改過的 store
?
對於 cfn3
來說, next
就是 store.dispatch
。我們先不考慮它為什麼是 next
, 但執行它了就會直接執行 store.dispatch
,後面立馬拿到修改後的資料不奇怪吧。
對於 cfn2
來說,next
就是 cfn3
執行後的返回值(執行後也還是個函式,內層並沒有執行),我們分為兩種情況:
cfn3
沒有執行next(action)
,那cfn1
cfn2
都沒法執行store.dispatch
,因為原始的dispatch
沒有傳遞下去,你會發現dispatch
函式被中介軟體搞失效了(所以中介軟體還可以搗亂)。為了防止中介軟體瞎搗亂,在中介軟體正常的情況請執行next(action)
.
這就是
redux-thunk
的核心思想,如果action
是個function
,就故意執行action
, 而不執行next(action)
, 等於讓store.dispatch
失效了!但其目的是明確的,因為會把dispatch
返回給使用者,讓使用者自己呼叫,正常使用是不會把流程停下來的。
cfn3
執行了next(action)
, 那cfn2
什麼時候執行next(action)
,cfn3
就什麼時候執行next(action) => store.dispatch(action)
, 所以這一步的next
效果與cfn3
相同,繼續傳遞下去也同理。我看了下redux-logger
的文件,果然央求使用者把自己放在最後一個,其原因是害怕最右邊的中介軟體『搗亂』,不執行next(action)
, 那logger
再執行next(action)
也無法真正觸發dispatch
.
我在考慮這樣會不會有很大的侷限性,但後來發現,只要中介軟體常規情況執行了
next(action)
就能保證原始的dispatch
可以被繼續分發下去。只要每個中介軟體都按照這個套路來,next(action)
的效果就與yield
類似。
所以 next
並不是完全意義上的洋蔥模型,只能說符合規範(預設都執行了 next(action)
)的中介軟體才符合洋蔥模型。
koa 的洋蔥模型可是有技術保證的,
generator
可不會受到程式碼的影響,而redux
中介軟體的洋蔥模型,會因為某一層不執行next(action)
而中斷,而且從右開始直接切斷。
為什麼在中介軟體直接 store.dispatch(action)
,傳遞就會中斷?
理解了上面說的話,就很簡單了,並不是 store.dispatch(action)
中斷了原始 dispatch
的傳遞,而是你執行完以後不呼叫 next
函式中斷了傳遞。
總結
還是要畫個圖總結一下,在不想看文字的時候:
本文對你有幫助?歡迎掃碼加入前端學習小組微信群: