為什么dispatch需要middleware
上圖表達(dá)的是 redux 中一個簡單的同步數(shù)據(jù)流動的場景劫扒,點擊 button 后,在回調(diào)中 dispatch 一個 action狸膏,reducer 收到 action 后沟饥,更新 state 并通知 view 重新渲染。單向數(shù)據(jù)流湾戳,看著沒什么問題贤旷。但是,如果需要打印每一個 action 信息用來調(diào)試砾脑,就得去改 dispatch
或者 reducer
代碼幼驶,使其具有打印日志的功能;又比如點擊 button
后韧衣,需要先去服務(wù)器請求數(shù)據(jù)盅藻,只有等拿到數(shù)據(jù)后,才能重新渲染 view
畅铭,此時我們又希望 dispatch
或者 reducer
擁有異步請求的功能氏淑;再比如需要異步請求完數(shù)據(jù)后,打印一條日志硕噩,再請求數(shù)據(jù)假残,再打印日志,再渲染...
面對多種多樣的業(yè)務(wù)需求榴徐,單純的修改 dispatch
或 reducer
的代碼顯然不具有普世性,我們需要的是可以組合的匀归,自由插拔的插件機制坑资,這一點 redux
借鑒了 koa
里中間件的思想,koa
是用于構(gòu)建 web 應(yīng)用的 NodeJS
框架穆端。另外 reducer
更關(guān)心的是數(shù)據(jù)的轉(zhuǎn)化邏輯袱贮,所以 redux
的 middleware
是為了增強 dispatch
而出現(xiàn)的。
上面這張圖展示了應(yīng)用 middleware 后 redux 處理事件的邏輯体啰,每一個 middleware 處理一個相對獨立的業(yè)務(wù)需求攒巍,通過串聯(lián)不同的 middleware嗽仪,實現(xiàn)變化多樣的的功能。那么問題來了:
* middleware 怎么寫柒莉?
* redux 是如何讓 middlewares 串聯(lián)并跑起來的闻坚?
理解middleware機制
redux 提供了 applyMiddleware 這個 api 來加載 middleware,為了方便理解兢孝,下圖將兩者的源碼放在一起進行分析窿凤。
圖下邊是 logger,打印 action 的 middleware跨蟹,圖上邊則是 applyMiddleware 的源碼雳殊,applyMiddleware 代碼雖然只有二十多行,卻非常精煉窗轩,接下來我們就分四步來深入解析這張圖夯秃。
-
第一步:函數(shù)式編程思想設(shè)計 middleware
middleware 的設(shè)計有點特殊,是一個層層包裹的匿名函數(shù)痢艺,這其實是函數(shù)式編程中的柯里化 curry仓洼,一種使用匿名單參數(shù)函數(shù)來實現(xiàn)多參數(shù)函數(shù)的方法。applyMiddleware 會對 logger 這個 middleware 進行層層調(diào)用腹备,動態(tài)地對 store 和 next 參數(shù)賦值衬潦。
柯里化的 middleware 結(jié)構(gòu)好處在于:
- 易串聯(lián),柯里化函數(shù)具有延遲執(zhí)行的特性植酥,通過不斷柯里化形成的 middleware 可以累積參數(shù)镀岛,配合組合( compose,函數(shù)式編程的概念友驮,Step. 2 中會介紹)的方式漂羊,很容易形成 pipeline 來處理數(shù)據(jù)流。
2.共享store卸留,在 applyMiddleware 執(zhí)行過程中走越,store 還是舊的,但是因為閉包的存在耻瑟,applyMiddleware 完成后旨指,所有的 middlewares 內(nèi)部拿到的 store 是最新且相同的。
另外喳整,我們可以發(fā)現(xiàn) applyMiddleware 的結(jié)構(gòu)也是一個多層柯里化的函數(shù)谆构,借助 compose , applyMiddleware 可以用來和其他插件一起加強 createStore 函數(shù).
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
)(createStore);
- 第二步 給 middleware 分發(fā) store
創(chuàng)建一個普通的 store 通過如下方式:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
上面代碼執(zhí)行完后框都,applyMiddleware 函數(shù)陸續(xù)獲得了三個參數(shù)搬素,第一個是 middlewares 數(shù)組,[mid1, mid2, mid3, ...],第二個 next 是 Redux 原生的 createStore熬尺,最后一個是 reducer摸屠。我們從對比圖中可以看到,applyMiddleware 利用 createStore 和 reducer 創(chuàng)建了一個 store粱哼,然后 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量季二,middlewareAPI 就是對比圖中紅色箭頭所指向的函數(shù)的入?yún)?store。
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
map 方法讓每個 middleware 帶著 middlewareAPI 這個參數(shù)分別執(zhí)行一遍皂吮,即執(zhí)行紅色箭頭指向的函數(shù)戒傻。執(zhí)行完后,獲得 chain 數(shù)組蜂筹,[f1, f2, ... , fx, ...,fn]需纳,它保存的對象是圖中綠色箭頭指向的匿名函數(shù),因為閉包艺挪,每個匿名函數(shù)都可以訪問相同的 store不翩,即 middlewareAPI。
-
第三步 組合串聯(lián) middlewares
dispatch = compose(...chain)(store.dispatch);
這一層只有一行代碼麻裳,但卻是 applyMiddleware 精華所在口蝠。compose 是函數(shù)式編程中的組合,compose 將 chain 中的所有匿名函數(shù),[f1, f2, ... , fx, ..., fn],組裝成一個新的函數(shù)呆抑,即新的 dispatch,當(dāng)新 dispatch 執(zhí)行時眉反,[f1, f2, ... , fx, ..., fn],從左到右依次執(zhí)行( 所以順序很重要)穆役。Redux 中 compose 的實現(xiàn)是下面這樣的寸五,當(dāng)然實現(xiàn)方式不唯一。
function compose(...funcs) { return arg => funcs.reduceRight((composed, f) => f(composed), arg); }
compose(...chain) 返回的是一個匿名函數(shù)耿币,函數(shù)里的 funcs 就是 chain 數(shù)組梳杏,當(dāng)調(diào)用 reduceRight 時,依次從 funcs 數(shù)組的右端取一個函數(shù) fx 拿來執(zhí)行淹接,fx 的參數(shù) composed 就是前一次 fx+1 執(zhí)行的結(jié)果十性,而第一次執(zhí)行的fn(n代表chain的長度)的參數(shù) arg 就是 store.dispatch。所以當(dāng) compose 執(zhí)行完后塑悼,我們得到的 dispatch 是這樣的劲适,假設(shè) n = 3。
dispatch = f1(f2(f3(store.dispatch))))
這個時候調(diào)用新 dispatch拢肆,每個 middleware 的代碼不就依次執(zhí)行了嘛.
- 第四步 在 middleware 中調(diào)用 dispatch 會發(fā)生什么
經(jīng)過 compose减响,所有的 middleware 算是串聯(lián)起來了,可是還有一個問題郭怪,我們有必要挖一挖支示。在 step 2 時,提到過每個 middleware 都可以訪問 store鄙才,即 middlewareAPI 這個變量颂鸿,所以就可以拿到 store 的 dispatch 方法,那么在 middleware 中調(diào)用 store.dispatch()會發(fā)生什么攒庵,和調(diào)用 next() 有區(qū)別嗎嘴纺?
在 step 2 的時候我們解釋過,通過匿名函數(shù)的方式浓冒,middleware 中 拿到的 dispatch 和最終 compose 結(jié)束后的新 dispatch 是保持一致的栽渴,所以在middleware 中調(diào)用 store.dispatch() 和在其他任何地方調(diào)用效果是一樣的,而在 middleware 中調(diào)用 next()稳懒,效果是進入下一個 middleware闲擦。
正常情況下當(dāng)我們 dispatch 一個 action 時,middleware 通過 next(action) 一層一層處理和傳遞 action 直到 redux 原生的 dispatch场梆。如果某個 middleware 使用 store.dispatch(action) 來分發(fā) action相當(dāng)于從外層重新來一遍墅冷,假如這個 middleware 一直簡單粗暴地調(diào)用 store.dispatch(action),就會形成無限循環(huán)了或油。那么 store.dispatch(action) 的勇武之地在哪里寞忿?正確的使用姿勢應(yīng)該是怎么樣的?舉個例子顶岸,需要發(fā)送一個異步請求到服務(wù)器獲取數(shù)據(jù)腔彰,成功后彈出一個自定義的 Message。這里我門用到了 redux-thunk 這個作者寫的 middleware蜕琴。
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
redux-thunk 做的事情就是判斷 action 類型是否是函數(shù)萍桌,若是,則執(zhí)行 action凌简,若不是上炎,則繼續(xù)傳遞 action 到下個 middleware。
針對上面的需求雏搂,我們設(shè)計了下面的 action:
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json';
fetch(url)
.then(response => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: response.json(),
});
}, e => {
dispatch({
type: 'FETCH_DATA_FAIL',
message: e,
});
});
};
這個時候只要在業(yè)務(wù)代碼里面調(diào)用 store.dispatch(getThenShow)藕施,redux-thunk 就會攔截并執(zhí)行 getThenShow 這個 action,getThenShow 會先請求數(shù)據(jù)凸郑,如果成功裳食,dispatch 一個顯示 Message 的 action,否則 dispatch 一個請求失敗的 action芙沥。這里的 dispatch 就是通過 redux-thunk middleware 傳遞進來的诲祸。
在 middleware 中使用 dispatch 的場景一般是:
接受到一個定向 action浊吏,這個 action 并不希望到達(dá)原生的 dsipatch,存在的目的是為了觸發(fā)其他新的 action救氯,往往用在異步請求的需求里找田。
總結(jié)
applyMiddleware 機制的核心在于組合 compose,將不同的 middlewares 一層一層包裹到原生的 dispatch 之上着憨,而為了方便進行 compose墩衙,需對 middleware 的設(shè)計采用柯里化 curry 的方式,達(dá)到動態(tài)產(chǎn)生 next 方法以及保持 store 的一致性甲抖。由于在 middleware 中漆改,可以像在外部一樣輕松訪問到 store, 因此可以利用當(dāng)前 store 的 state 來進行條件判斷,用 dispatch 方法攔截老的 action 或發(fā)送新的 action准谚。
github
最后挫剑,希望這篇博客對大家有所幫助(如果是,請盡情star哦柱衔,??)暮顺,歡迎提出您的寶貴建議~