深入淺出redux-middleware

多數(shù)redux初學(xué)者都會使用redux-thunk這個中間件來處理異步請求(比如我)

本來寫這篇文章只是想寫寫redux-thunk间狂,然后發(fā)現(xiàn)還不夠瑟幕,就順便把middleware給過了一遍学辱。

為什么叫thunk?

thunk是一種包裹一些稍后執(zhí)行的表達(dá)式的函數(shù)姑裂。

redux-thunk源碼

所有的代碼就只有15行钮惠,我說的是真的僻他。。 redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

代碼很精簡,但是功能強(qiáng)大坤候,所以非常有必要去了解一下。

redux-middleware是個啥

image

上圖描述了一個redux中簡單的同步數(shù)據(jù)流動的場景址貌,點(diǎn)擊button后铐拐,dispatch一個action,reducer 收到 action 后练对,更新state后告訴UI遍蟋,幫我重新渲染一下。

redux-middleware就是讓我們在dispatch action之后螟凭,在action到達(dá)reducer之前虚青,再做一點(diǎn)微小的工作,比如打印一下日志什么的螺男。試想一下棒厘,如果不用middleware要怎么做,最navie的方法就是每次在調(diào)用store.dispatch(action)的時候下隧,都console.log一下actionnext State奢人。

store.dispatch(addTodo('Use Redux'));
  • naive的方法,唉淆院,每次都寫上吧
const action = addTodo('Use Redux');

console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
  • 既然每次都差不多何乎,那封裝一下吧
function dispatchAndLog(store, action) {
  console.log('dispatching', action);
  store.dispatch(action);
  console.log('next state', store.getState());
}
  • 現(xiàn)在問題來了,每次dispatch的時候都要import這個函數(shù)進(jìn)來土辩,有點(diǎn)麻煩是不是支救,那怎么辦呢?

既然dispatch是逃不走的拷淘,那就在這里動下手腳各墨,reduxstore就是一個有幾種方法的對象,那我們就簡單修改一下dispatch方法启涯。

const next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action); // 之前是 `dispatch(action)`
  console.log('next state', store.getState());
}

這樣一來我們無論在哪里dispatch一個action贬堵,都能實(shí)現(xiàn)想要的功能了恃轩,這就是中間件的雛形。

image
  • 現(xiàn)在問題又來了扁瓢,大佬要讓你加一個功能咋辦详恼?比如要異常處理一下

接下來就是怎么加入多個中間件了。

function patchStoreToAddLogging(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}

patchStoreToAddLoggingpatchStoreToAddCrashReportingdispatch進(jìn)行了重寫引几,依次調(diào)用這個兩個函數(shù)之后昧互,就能實(shí)現(xiàn)打印日志和異常處理的功能。

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
  • 之前我們寫了一個函數(shù)來代替了store.dispatch伟桅。如果直接返回一個新的dispatch函數(shù)呢敞掘?
function logger(store) {
  const next = store.dispatch

  // 之前:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

這樣寫的話我們就需要讓store.dispatch等于這個新返回的函數(shù),再另外寫一個函數(shù)楣铁,把上面兩個middleware連接起來玖雁。

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}

middleware(store)會返回一個新的函數(shù),賦值給store.dispatch盖腕,下一個middleware就能拿到一個的結(jié)果赫冬。

接下來就可以這樣使用了,是不是優(yōu)雅了一些溃列。

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

我們?yōu)槭裁催€要重寫dispatch呢劲厌?當(dāng)然啦,因為這樣每個中間件都可以訪問或者調(diào)用之前封裝過的store.dispatch听隐,不然下一個middleware就拿不到最新的dispatch了补鼻。

function logger(store) {
  // Must point to the function returned by the previous middleware:
  const next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

連接middleware是很有必要的。

但是還有別的辦法雅任,通過柯里化的形式风范,middlewaredispatch作為一個叫next的參數(shù)傳入,而不是直接從store里拿沪么。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

柯里化就是把接受多個參數(shù)的函數(shù)編程接受一個單一參數(shù)(注意是單一參數(shù))的函數(shù)硼婿,并返回接受余下的參數(shù)且返回一個新的函數(shù)。

舉個例子:

const sum = (a, b, c) => a + b + c;

// Curring
const sum = a => b => c => a + b + c;

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
  }
}

接下來我們就可以寫一個applyMiddleware了。

// 注意:這是簡單的實(shí)現(xiàn)
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  return Object.assign({}, store, { dispatch })
}

上面的方法哭当,不用立刻對store.dispatch賦值,而是賦值給一個變量dispatch冗澈,通過dispatch = middleware(store)(dispatch)來連接钦勘。

現(xiàn)在來看下reduxapplyMiddleware是怎么實(shí)現(xiàn)的?

applyMiddleware

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
 
 // 就是把上一個函數(shù)的返回結(jié)果作為下一個函數(shù)的參數(shù)傳入亚亲, compose(f, g, h)和(...args) => f(g(h(...args)))等效

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最后返回的也是一個函數(shù)彻采,接收一個參數(shù)args腐缤。

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)
    }
    
    // 確保每個`middleware`都能訪問到`getState`和`dispatch`
    
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // wrapDispatchToAddLogging(store)
    dispatch = compose(...chain)(store.dispatch)
    
    // wrapCrashReport(wrapDispatchToAddLogging(store.dispatch))

    return {
      ...store,
      dispatch
    }
  }
}

image

借用一下大佬的圖, google搜索redux-middleware第一張

到這里我們來看一下applyMiddleware是怎樣在createStore中實(shí)現(xiàn)的。

export default function createStore(reducer, preloadedState, enhancer){
  ...
}

createStore接受三個參數(shù):reducer, initialState, enhancer肛响。enhancer就是傳入的applyMiddleware函數(shù)岭粤。

createStore-enhancer #53

//在enhancer有效的情況下,createStore會返回enhancer(createStore)(reducer, preloadedState)特笋。
return enhancer(createStore)(reducer, preloadedState)

我們來看下剛剛的applyMiddleware剃浇,是不是一下子明白了呢。

return createStore => (...args) => {
    // ....
}

到這里應(yīng)該就很容易理解redux-thunk的實(shí)現(xiàn)了猎物,他做的事情就是判斷action 類型是否是函數(shù)虎囚,如果是就執(zhí)行action,否則就繼續(xù)傳遞action到下個 middleware蔫磨。

參考文檔:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淘讥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子堤如,更是在濱河造成了極大的恐慌蒲列,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搀罢,死亡現(xiàn)場離奇詭異蝗岖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)魄揉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門剪侮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洛退,你說我怎么就攤上這事瓣俯。” “怎么了兵怯?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵彩匕,是天一觀的道長。 經(jīng)常有香客問我媒区,道長驼仪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任袜漩,我火速辦了婚禮绪爸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宙攻。我一直安慰自己奠货,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布座掘。 她就那樣靜靜地躺著递惋,像睡著了一般柔滔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萍虽,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天睛廊,我揣著相機(jī)與錄音,去河邊找鬼杉编。 笑死超全,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的王财。 我是一名探鬼主播卵迂,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绒净!你這毒婦竟也來了见咒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤挂疆,失蹤者是張志新(化名)和其女友劉穎改览,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缤言,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宝当,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胆萧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庆揩。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖跌穗,靈堂內(nèi)的尸體忽然破棺而出订晌,到底是詐尸還是另有隱情,我是刑警寧澤蚌吸,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布锈拨,位于F島的核電站,受9級特大地震影響羹唠,放射性物質(zhì)發(fā)生泄漏奕枢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一佩微、第九天 我趴在偏房一處隱蔽的房頂上張望缝彬。 院中可真熱鬧,春花似錦哺眯、人聲如沸跌造。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽壳贪。三九已至,卻和暖如春寝杖,著一層夾襖步出監(jiān)牢的瞬間违施,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工瑟幕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磕蒲,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓只盹,卻偏偏與公主長得像辣往,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子殖卑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348

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