redux middleware 的理解


前言

這幾天看了redux middleware的運用與實現原理,寫了一個百度搜索的demo,實現了類似redux-thunk和redux-logger中間件的功能。

項目地址:https://github.com/CanFoo/react-baidu-search/tree/master

 

redux中間件是通過函數式編程實現,因此要閱讀源碼需要有一定函數式編程基礎,比如柯里化函數的實現,否則難以理解源碼的緣由。接下去通過這個demo給大家講解個人對中間件的理解,如有問題,come on 指正。

redux middleware是什么

如果沒有中間件的運用,redux 的工作流程是這樣 action -> reducer,這是相當於同步操作,由dispatch 觸發action后,直接去reducer執行相應的動作。但是在某些比較復雜的業務邏輯中,這種同步的實現方式並不能很好的解決我們的問題。比如我們有一個這樣的需求,點擊按鈕 -> 獲取服務器數據 -> 渲染視圖,因為獲取服務器數據是需要異步實現,所以這時候我就需要引入中間件改變redux同步執行的流程,形成異步流程來實現我們所要的邏輯,有了中間件,redux 的工作流程就變成這樣 action -> middlewares -> reducer,點擊按鈕就相當於dispatch 觸發action,接下去獲取服務器數據 middlewares 的執行,當 middlewares 成功獲取到服務器就去觸發reducer對應的動作,更新需要渲染視圖的數據。中間件的機制可以讓我們改變數據流,實現如異步 action ,action 過濾,日志輸出,異常報告等功能。

如何自定義中間件

redux 提供了一個叫 applyMiddleware 的方法,可以應用多個中間件,這樣就可以當觸發action時候就會被這些中間件給捕獲,這邊我們分別定義了搜索和日志的中間件,而且中間件傳入順序是先搜索中間件再日志打印中間件,這個順序是有講究的,下文會說明的

import { createStore } from 'redux'
import applyMiddleware from './applyMiddleware/applyMiddleware'
import compose from './applyMiddleware/compose'
import reducer from './reducers'
import loggerMiddleware from './middlewares/loggerMiddleware'
import searchMiddleware from './middlewares/searchMiddleware'

const createStoreWithMiddleware = compose(
    applyMiddleware(
        searchMiddleware,
        loggerMiddleware
    ),
    window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore)

searchMiddleware是搜索中間件,其實它就是仿照redux-thunk的實現

export default function thunkMiddleware({ dispatch, getState }) {
  return next => action => {
    /*如果action是一個函數,則先執行action,否則通過next進入到下一個action對應的reducer*/
    typeof action === 'function' ?
      action(dispatch, getState) : //這里action(dispatch, getState)指的是getThenShow返回函數的執行
      next(action);
}}

我們先來分析下中間件的執行順序,下文再來解釋為什么這里結構是() => next => action => {}以及源碼是如何實現中間件鏈。這里的代碼非常簡單,就是一個三目運算符,當action是一個函數類型,就直接執行這個action函數,否則就通過next來鏈接到下一個組件,next是專門用來串聯組件間的執行順序。我們知道,如果沒有中間件的處理,action只能返回一個對象格式,否則reducer不能進行處理action傳過來的行為,但是有了中間件,我們就可以“肆意妄為”,只要保證最終傳給reducer的action是一個對象,期間從觸發action到真正到達reducer我想要action是什么就可以是什么,下面是搜索邏輯所對應的action

export function getThenShow(value) {
  return dispatch => {
    const url = 'https://gsp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su';
    jsonp(url, {wd: value}, 'cb', (data) => {
        dispatch({
          type: SHOW_MESSAGE_SUCESS,
          lists: data.s
        });
    })
  }
}

當執行dispatch(getThenShow())時,上文所說的searchMiddleware就會捕獲到這個action,因為第一次傳過來的action是

functon(dispatch) {
    const url = 'https://gsp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su';
    jsonp(url, {wd: value}, 'cb', (data) => {
        dispatch({
          type: SHOW_MESSAGE_SUCESS,
          lists: data.s
    });
})

如上所示,因為第一次傳過來的action是function類型的,所以searchMiddleware里就會先執行這個action函數,這個aciton執行jsonp方法回調另一個dispatch,從上面代碼可以看出這個時候dispatch分發的action是一個對象,type為SHOW_MESSAGE_SUCCESS,lists為搜索結果,因此下一次searchMiddleware捕獲到的action為一個對象,從而去執行next(action),也就是去執行下一個中間件loggerMiddleware的內容

export default function createLogger({ getState }) {
    return (next) => (action) => {
        const prevState = getState();
        const returnValue = next(action);
        const nextState = getState();
        console.log(`%c prev state`, `color: #9E9E9E`, prevState);
        console.log(`%c action`, `color: #03A9F4`, action);
        console.log(`%c next state`, `color: #4CAF50`, nextState);
        console.log('===============')
        return returnValue;
    };
}

loggerMiddleware里先通過getState()獲取到當前狀態,接着用通過next(action)執行下一個中間件,由於applyMiddleware傳入中間件的順序是先searchMiddleware再loggerMiddleware,而且loggerMiddleware之后沒有其它中間件傳入,因此此時的next指的是原生的dispatch,進而會去觸發reducer所對應的動作,所以再次調用getSatate()會返回下一個狀態內容。

所以當在搜索框輸入內容redux執行的步驟為:

1.輸入內容觸發dispatch(getThenShow())發起第一個action(這里getThenShow()返回的action的是一個函數)

2.searchMiddleware捕獲到第一個action,因為action返回是一個函數,所以執行action

3.第一個action執行完后回調再次觸發dispatch({type: SHOW_MESSAGE_SUCCESS, lists: ...})發起第二個action

4.searchMiddleware捕獲到第二個action,執行next(action),鏈接到loggerMiddleware

5.loggerMiddleware獲取當前(prev)狀態后執行next(action)從而觸發reducer的對應的動作

6.loggerMiddleware再次獲取當前(next)狀態,然后打印出狀態,執行完loggerMiddleware程序

7.回溯到searchMiddleware,searchMiddleware程序執行完

8.整個redux中間件執行完

到這里,我們就把這個demo的中間件執行順序分析完,那么為什么自定義中間件是() => next => action => {}這樣的結構?next又是如何將中間件串聯起來的?其實這個兩個問題是同一個問題,因為中間件設定這樣() => next => action => {}的結構目的就是為了把中間件串聯起來的。為了一探究竟,我們還是得來來看看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
    }
  }
}

從applyMiddleware執行完后最終返回的也是一個閉包函數,將創建 store的步驟放在這個閉包內執行,這樣中間件就可以共享 store 對象。applyMiddleware是這樣來對傳進來中間件進行函數式編程處理的

1.通過...middlewares將所有的中間件存入到middlewares數組中

2.middlewares 數組通過 map 方法執行生成新的 middlewares 數組且每一項都傳入middlewareAPI,傳入middlewareAPI的目的就使得每個中間件都可以訪問到store,這時候middlewares數組的每一項都變為了

function (next) {
    return function (action) {...}
}

3.compose 方法將新的 middlewares 和 store.dispatch 結合起來,生成一個新的 dispatch 方法。這里的關鍵點是compose,我們來看看compose的設計

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  
  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  const fn = (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  return fn
}

可以看到 compose 方法實際上就是利用Array.prototype.reduceRight來進行下一步處理的。如果對reduceRight方法不了解的童鞋,可以先看看這里。我們這里可以來模擬一下compose函數處理完的結果,假設我們這邊有兩個中間件A和B,則傳入到compose的func為[A, B],且A、B的形式已經是(next) => (action) => {}

function A(next) {
    console.log('A...next === ', next)
    return function(action) {
        console.log('A...action')
        next(action)
    }
}
function B(next) {
    console.log('B...next === ', next)
    return function(action) {
        console.log('B...action')
        next(action)
    }
}

function compose(funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  const fn = (args) => rest.reduceRight((composed, f) => f(composed), last(args))
  return fn
}

var fnArr = [A, B]
var dispatch = compose(fnArr)("store.dispatch")
console.log('new dispatch === ', dispatch)

執行的結果是

由結果可以看到中間件A的next是指向中間件B的最內層閉包函數,而中間件B的next則是指向原生的dispatch,所以通過compose執行完后,所有的中間件就通過next串聯起來了。這也就是為什么我們所分析這個百度搜索demo中的searchMiddleware的next是指向loggerMiddleware,而loggerMiddleware的next指向原生dispatch的原因。

4.返回的 store 新增了一個 dispatch 方法, 這個新的 dispatch 方法是改裝過的 dispatch,由上例中這個改裝過的 dispatch就是指的是中間件A最里層的閉包函數,這也就是為什么說有了中間件就可以捕獲action的行為的原理。

到此applyMiddleware源碼分析完畢,我們也可以明白為什么自定義組件需要設計成() => next => action => {}的形式,其實也就是設計成柯里化方式,因為這樣方便進行compose,從而達到動態產生 next 方法以及保持 store 的一致性。

 


教程源代碼地址

https://github.com/CanFoo/react-baidu-search/tree/master

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM