思考
我們在 Redux 異步 Action 中經(jīng)常使用這種寫法:
export function getTodos(){
return (dispatch,getStore) => {
TodoApi.getTodos().then(result=>{
dispatch({
type:TODOLIST,
data:result.data
})
})
}
}
而正常的 Action 就看起來好像應(yīng)該是這樣:
export function getTodos(){
return {
type:TODOLIST,
data:result.data
}
}
為什么我們上一種寫法能正常工作呢?Action不應(yīng)該返回一個對象嗎怎么返回了一個函數(shù)儒鹿?
Action 之所以支持第一種寫法轻局,是因為我們引入了 redux-thunk
中間件诲祸。那么 redux-thunk
是怎么工作的歉铝?
讓我們在本文中一步步剖析 middleware
中間件的實現(xiàn)原理。
什么是 middleware
middleware
是指可以被嵌入在框架接收請求到產(chǎn)生響應(yīng)過程之中的代碼谍婉。middleware
最優(yōu)秀的特性就是可以被鏈式組合更啄。我們可以在一個項目中使用多個獨立的第三方 middleware稚疹。
正因為 middleware 可以完成包括異步 API 調(diào)用在內(nèi)的各種事情,了解它的演化過程是一件相當(dāng)重要的事祭务。
我們將以記錄日志和創(chuàng)建崩潰報告為例内狗,體會從分析問題到通過構(gòu)建 middleware 解決問題的思維過程。
問題:記錄日志
現(xiàn)在設(shè)想這么一個問題:我們需要在應(yīng)用中每一個 Action
被發(fā)起以及新的 state
被計算完成時都將他們記錄下來义锥。當(dāng)程序出現(xiàn)問題時柳沙,我們可以通過查閱日志找出是哪個 action 導(dǎo)致了 state 不正確。
嘗試 1: 手動記錄
假設(shè)拌倍,我們在獲取Todo列表時這么調(diào)用
export function getTodos(){
return {
type:TODOLIST,
data:result.data
}
}
store.dispatch(getTodos())
為了記錄這個action以及新的state赂鲤,我們可以通過這種方式記錄:
let action = getTodos();
console.log('dispatching', action)
store.dispatch(action)
雖然這么做可以達到想要的效果,可是我們并不想每次都這么多柱恤。
嘗試 2: 封裝 Dispatch
我們可以將上面的操作封裝成一個函數(shù)
function dispatchWithLog(store,action){
console.log('dispatching', action)
store.dispatch(action)
}
然后我們這么調(diào)用它:
dispatchWithLog(store, getTodos())
我們已經(jīng)接近了 middleware 的思想数初,但每次都要導(dǎo)入一個外部方法總歸不大方便。
嘗試 3: 替換 Dispatch
如果我們直接替換
store 實例中的 dispatch 函數(shù)會怎么樣呢梗顺?
Redux store 只是一個包含一些方法的普通對象泡孩,同時我們使用的是 JavaScript,因此我們可以這樣來包裝 dispatch:
let next = store.dispatch
store.dispatch = function dispatchWithLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
- 將 next 指向原生的dispatch寺谤。
- 將 store.diapatch 變成我們自定義的函數(shù)珍德。
- 在這個自定義的函數(shù)中調(diào)用next,也就是原dispatch矗漾。
這樣就完美地改寫了dispatch,保留了原始功能薄料,還添加了自定義的方法敞贡。離我們想要的已經(jīng)非常接近了!
但直接替換 dispatch 令人感覺還是不太舒服摄职,不過利用它我們做到了我們想要的誊役。
問題: 捕獲異常
如果我們想對 dispatch 附加超過一個的特殊處理获列,又會怎么樣呢?
腦海中出現(xiàn)的另一個常用的特殊處理就是在生產(chǎn)過程中報告 JavaScript 的錯誤蛔垢。
但是全局的 window.onerror
并不可靠击孩,因為它在一些舊的瀏覽器中無法提供錯誤堆棧,而這是排查錯誤所需的至關(guān)重要信息鹏漆。
試想當(dāng)發(fā)起一個 action 的結(jié)果是一個異常時巩梢,我們將包含調(diào)用堆棧,引起錯誤的 action 以及當(dāng)前的 state 等錯誤信息通通發(fā)到報告服務(wù)中艺玲,不是很好嗎括蝠?這樣我們可以更容易地在開發(fā)環(huán)境中重現(xiàn)這個錯誤。
然而饭聚,將日志記錄和崩潰報告 分離
是很重要的忌警。理想情況下,我們希望他們是兩個不同的模塊秒梳,也可能在不同的包中法绵。否則我們無法構(gòu)建一個由這些工具組成的生態(tài)系統(tǒng)。
按照我們的想法酪碘,日志記錄和崩潰報告屬于不同的模塊朋譬,他們看起來應(yīng)該像這樣:
function patchStoreWithLog(store) {
let next = store.dispatch
store.dispatch = function dispatchWithLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function patchStoreWithReport(store) {
let next = store.dispatch
store.dispatch = function dispatchWithReportErrors(action) {
try {
return next(action)
} catch (error) {
console.error('捕獲一個異常!', error)
report({
error,
action,
state: store.getState()
})
throw error
}
}
}
如果這些功能以不同的模塊發(fā)布,我們可以在 store 中像這樣使用它們:
patchStoreWithLog(store)
patchStoreWithReport(store)
- 第一個
patchStoreWithLog
將dispatch進行了第一層封裝婆跑,他的next
是store原生dispatch - 第二個
patchStoreWithReport
將dispatch進行了第二次封裝此熬,他的next
是dispatchWithLog
這樣我們就實現(xiàn)了對dispatch的多重處理。
盡管如此滑进,這種方式看起來還是有一些啰嗦犀忱。
嘗試 4: 隱藏 hack
我們之前的操作本質(zhì)上是一種hack。
我們用自己的函數(shù)替換掉了 store.dispatch扶关。如果我們不這樣做阴汇,而是在函數(shù)中返回新的 dispatch 呢?
function logger(store) {
let next = store.dispatch
// 我們之前的做法:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchWithLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function report(store){
let next = store.dispatch
// 我們之前的做法:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchWithReport(action) {
try{
let result = next(action)
return result
}catch (error) {
console.error('捕獲一個異常!', error)
report({
error,
action,
state: store.getState()
})
throw error
}
}
}
我們通過閉包
存儲了store
节槐,以便在action真正調(diào)用的時候供其訪問搀庶。
我們可以在 Redux 內(nèi)部提供一個可以將實際的 hack 應(yīng)用到 store.dispatch 中的輔助方法:
function applyMiddleware(store, middlewares) {
// 在每一個 middleware 中變換 dispatch 方法。
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
然后像這樣應(yīng)用多個 middleware:
applyMiddleware(store, [ logger,report ])
上面的代碼可能看起來一時難以理解铜异,這也是中間件的精華所在哥倔。我們來具體分析一下:
middleware
每一個middleware
都是高階函數(shù),它使用了函數(shù)柯里化
的思想揍庄,分兩步執(zhí)行:
- 第一步:接收一個
store
對象咆蒿,利用閉包將其存儲。返回一個函數(shù),也就是第二步沃测。 - 第二步:接收一個具體的
action
缭黔,使用上一步存儲的store.dispatch
執(zhí)行該action
,并返回執(zhí)行結(jié)果。
applyMiddleware
接收原始的store
對象和一系列的middlewares
蒂破。
它遍歷 middlewares 列表馏谨,進行以下操作:
對每一個 middleware 進行第一步調(diào)用,入?yún)?code>store對象附迷,返回一個可執(zhí)行函數(shù)惧互。
將 store.dispatch
hack
為 可執(zhí)行函數(shù)。
這樣一來挟秤,每一個 middleware 所存儲的 store 壹哺,都是被
上一個
middleware hack后的。也就是說艘刚,
我們在 report 中閉包存儲的store對象管宵,它的 dispatch 方法實際上是 logger 的dispatchWithLog
。
總體流程
調(diào)用
store.dispatch
攀甚,實際上調(diào)用的是dispatchWithReport
進行錯誤信息收集箩朴,然后調(diào)用
next(action)
,實際上調(diào)用的是dispatchWithLog(action)
進行日志記錄,然后調(diào)用
next(action)
秋度,此時才真正調(diào)用原生store.dispatch
原生的 dispatch 返回一個 Reducer 可識別的對象炸庞,層層向外傳遞,交由 Reducer處理荚斯。
嘗試 #5: 移除 hack
為什么我們要替換原來的 dispatch 呢 埠居?
就是每一個 middleware 都可以操作前一個 middleware 包裝過的 store.dispatch。
如果沒有在第一個 middleware 執(zhí)行時立即替換掉 store.dispatch事期,
那么 store.dispatch 將會一直指向原始的 dispatch 方法滥壕。也就是說,第二個 middleware 依舊會作用在原始的 dispatch 方法兽泣。
還有另一種方式來實現(xiàn)這種鏈式調(diào)用的效果绎橘。就是將middleware柯里化
為三步,
讓 middleware 以方法參數(shù)的形式接收一個 next() 方法唠倦,而不是通過 store 的實例去獲取称鳞,這樣我們就可以避免對store.dispatch
的hack
function logger(store) {
return function wrapDispatch(next) {
return function dispatchWithLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}
這些串聯(lián)函數(shù)很嚇人。ES6 的箭頭函數(shù)可以使 柯里化
看起來更舒服一些:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
這正是 Redux middleware 的樣子稠鼻。
如果要自己實現(xiàn)一個 middleware
應(yīng)用到 redux 中冈止,完全可以按照這種形式去寫。
源碼分析
我們的applyMiddleware
和 Redux 中 applyMiddleware()
的實現(xiàn)已經(jīng)很接近了候齿。有了上面的鋪墊熙暴,讓我們來分析一下真正的源碼苫亦。
代碼雖然只有不到20行,但看懂確實是不容易怨咪。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
)
}
const middlewareAPI = { // 定義API
getState: store.getState, //注入 getStore方法
dispatch: (...args) => dispatch(...args) //初始化dispatch
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
middlewares
export default function applyMiddleware(...middlewares)
applyMiddleware
接收第一個參數(shù),他正是所有的middleware列表润匙。
createStore 以及 reducers
return createStore => (...args) => {
... ...
}
applyMiddleware
接收第二和第三個參數(shù)诗眨,他們分別是createStore
reducers
為了保證只能應(yīng)用 middleware 一次并闲,它作用在 createStore() 上而不是 store 本身恬惯。
創(chuàng)建store
const store = createStore(...args)
利用傳入的createStore和reducer和創(chuàng)建一個store
定義API
let dispatch = () => {
throw new Error(
)
}
const middlewareAPI = { // 定義API
getState: store.getState, //注入 getStore方法
dispatch: (...args) => dispatch(...args) //初始化dispatch
}
這里有一個地方需要注意:
并沒有直接使用
dispatch:dispatch
壕鹉,而是使用了dispatch:(action) => dispatch(action)
如果使用了
dispatch:dispatch
摇天,那么在所有的 Middleware 中實際都引用的同一個dispatch(閉包)泞歉,
那么一個中間件修改了dispatch新症,其他所有的dispatch都將被改變罢低。所以這里使用
dispatch:(action) => dispatch(action)
说墨,每一個 middlewareAPI 的 dispatch 引用都是不同的
初始化
const chain = middlewares.map(middleware => middleware(middlewareAPI))
讓每個 middleware 帶著 middlewareAPI 這個參數(shù)分別執(zhí)行一遍璃饱,進行初始化与斤。
得到的函數(shù)鏈為每個中間件的第一個返回函數(shù),該函數(shù)可接收一個dispatch動作荚恶,再返回一個可以接收action的函數(shù)撩穿。
函數(shù)鏈中的每一個函數(shù)看起來像是這樣:
middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 這里 middlewareAPI 作為閉包存在于匿名函數(shù)Anonymous的作用域鏈
function Anonymous (next) {
return function(action){
... ...
return next(action);
}
}
它的柯里化后兩步操作分別為:
- 接收一個next方法
- 接受一個action,并執(zhí)行
next(action)
compose
dispatch = compose(...chain,store.dispatch)
這句是最精妙也是最有難度的地方谒撼。
個人認為這個compose函數(shù)是整個redux中非常亮眼的部分食寡,
短短幾行代碼,就完成了一個核心功能的擴展廓潜,是責(zé)任鏈設(shè)計模式的經(jīng)典體現(xiàn)抵皱。
我們來看一下compose
的源碼:
function compose() {
for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
funcs[_key] = arguments[_key];
}
if (funcs.length === 0) {
return function (arg) {
return arg;
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}
compose
在這里將所有的 中間件的第一個返回函數(shù)
聚合。
也就像我們剛才分析的辩蛋,將store.dispatch
傳給第一個中間件呻畸,第一個中間件對其進行封裝后傳給第二個中間件,
以此類推... ...堪澎。
最底層的dispatch
為store.dispatch
擂错,一層一層的封裝,最終得到一個層層封裝后的“dispatch”
樱蛤。
或許已經(jīng)不能稱之為dispatch
钮呀,他是原生的dispatch
,以及一系列增強函數(shù)的集合
昨凡。
最后
return {
...store,
dispatch
}
將store
中的所有可枚舉屬性復(fù)制進去(淺復(fù)制),并用層層封裝好的“dispatch”
覆蓋store中的dispatch屬性爽醋。
redux-thunk
我們回到最開始的問題
export function getTodos(){
return (dispatch,getStore) => {
TodoApi.getTodos().then(result=>{
dispatch({
type:TODOLIST,
data:result.data
})
})
}
}
上面這種寫法是怎么工作的呢?
讓我們結(jié)合redux-thunk源碼來分析一下便一目了然便脊。
function createThunkMiddleware(extraArgument) {
return function (_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function (next) {
return function (action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
代碼同樣精煉蚂四,改造后的dispatch接收到action后有兩種情況:
- 如果我們返回的
action
是一個普通對象,形如
{
type:GETTODOS,
data:[]
}
那么,redux-thunk將不予處理遂赠,繼續(xù)將 action 向下傳遞久妆。項目中如果沒有其他中間件,這里會直接調(diào)用原生的dispatch跷睦,交由 Reducer 處理筷弦。
- 如果我們返回的
action
是一個函數(shù),就像我們一直在使用的:
return (dispatch,getStore) => {
TodoApi.getTodos().then(result=>{
dispatch({
type:TODOLIST,
data:result.data
})
})
}
為了方便理解抑诸,我們可以稍作變換:
const func = (dispatch, getStore) => {
TodoApi.getTodos().then(result => {
dispatch({
type: TODOLIST,
data: result.data
})
})
}
return func;
這里的返回值無疑是function
±们伲現(xiàn)在就輪到redux-thunk
上場了:
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
這里的action
就是我們的func
,func
在此處被執(zhí)行,就相當(dāng)于:
func(dispatch, getState, extraArgument);
這里的dispatch
蜕乡,getState
都是在閉包中存儲的變量奸绷。
-
getState
可以獲取到store中所有的state -
dispatch
可能是store.dispatch
原生方法,當(dāng)然也有可能是下一個 middleware 封裝后的方法层玲。
看到這里号醉,應(yīng)該不難明白這是如何工作的了吧:
export function getTodos(){
return (dispatch,getStore) => {
TodoApi.getTodos().then(result=>{
dispatch({
type:TODOLIST,
data:result.data
})
})
}
}