深入理解Redux中間件

思考

我們在 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進行了第二次封裝此熬,他的nextdispatchWithLog

這樣我們就實現(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傳給第一個中間件呻畸,第一個中間件對其進行封裝后傳給第二個中間件,
以此類推... ...堪澎。

最底層的dispatchstore.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
      })
    })
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市称簿,隨后出現(xiàn)的幾起案子扣癣,更是在濱河造成了極大的恐慌,老刑警劉巖憨降,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件父虑,死亡現(xiàn)場離奇詭異,居然都是意外死亡授药,警方通過查閱死者的電腦和手機士嚎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悔叽,“玉大人莱衩,你說我怎么就攤上這事〗颗欤” “怎么了笨蚁?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長趟庄。 經(jīng)常有香客問我括细,道長,這世上最難降的妖魔是什么戚啥? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任奋单,我火速辦了婚禮,結(jié)果婚禮上猫十,老公的妹妹穿的比我還像新娘览濒。我一直安慰自己呆盖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布贷笛。 她就那樣靜靜地躺著应又,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乏苦。 梳的紋絲不亂的頭發(fā)上丁频,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音邑贴,去河邊找鬼。 笑死叔磷,一個胖子當(dāng)著我的面吹牛拢驾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播改基,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼繁疤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秕狰?” 一聲冷哼從身側(cè)響起稠腊,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸣哀,沒想到半個月后架忌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡我衬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年叹放,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挠羔。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡井仰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出破加,到底是詐尸還是另有隱情俱恶,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布范舀,位于F島的核電站合是,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尿背。R本人自食惡果不足惜端仰,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望田藐。 院中可真熱鬧荔烧,春花似錦吱七、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至臀稚,卻和暖如春吝岭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吧寺。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工窜管, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稚机。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓幕帆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赖条。 傳聞我的和親對象是個殘疾皇子失乾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 前言 最近幾天對 redux 的中間件進行了一番梳理,又看了 redux-saga 的文檔纬乍,和 redux-thu...
    Srtian閱讀 32,703評論 9 40
  • “中間件”這個詞聽起來很恐怖碱茁,但它實際一點都不難。想更好的了解中間件的方法就是看一下那些已經(jīng)實現(xiàn)了的中間件是怎么工...
    Jmingzi_閱讀 1,676評論 1 7
  • 寫在開頭 本片內(nèi)容主要為本人在閱讀redux官方文檔中基礎(chǔ)和進階部分的學(xué)習(xí)筆記仿贬。由于本人能力有限纽竣,所以文章中可能會...
    前端開發(fā)愛好者閱讀 1,188評論 0 4
  • 一、中間件的概念 redux是有流程的茧泪,那么退个,我們該把這個異步操作放在哪個環(huán)節(jié)比較合適呢? Reducer?純函數(shù)...
    java高并發(fā)閱讀 331評論 0 0
  • 有時候我會經(jīng)常問自己,喜歡寫字缰泡,不停地寫字刀荒,到底在寫些什么,寫字它能帶給我什么棘钞。 想起剛開始要我寫字的時候缠借,我是拒...
    6月姑娘閱讀 336評論 0 0