寫給本身看的React源碼解析(五):前端必學-Redux中間件的實現原理

前言

中間件的內容其實不屬於React源碼相關,屬於Redux相關。可是中間件的原理是一個很是重要的知識點,它是咱們前端開發解決一些業務問題時的利器。不少前端應用的架構都是使用中間件爲基礎搭建的。前端

本文不會介紹Redux相關的內容,只關注於中間件的實現原理。react

本文將是我學習react源碼的目前階段的最後一篇文章,react源碼內容比較多,也比較晦澀,我也不可以一蹴而就。等過段時間再繼續深刻學習的時候,再來接着更新這一系列的內容。git

認識Redux中間件

React開發中,管理數據狀態的Redux是每一個人都會接觸到的內容(如同VuexVue開發當中的地位)。咱們知道,在Redux中,咱們想要修改數據,咱們必須先派發一個ActionAction會被Reducer讀取,Reducer將根據Action內容的不一樣執行不一樣的計算邏輯,最終生成新的state,這個新的state會更新到Store對象裏,進而驅動視圖層面做出對應的改變。github

這裏有一個須要注意的地方,Redux源碼中只有同步操做,也就是說當咱們dispatch action時,state會被當即更新。編程

若是咱們想引入異步數據流,該怎麼辦?官方的建議就是使用中間件。redux

本文使用redux-thunk做爲處理異步數據流的方式。源碼地址數組

import thunkMiddleware from 'redux-thunk'
import reducer from './reducers'
// 使用redux-thunk中間件
const store = createStore(reducer, applyMiddleware(thunkMiddleware))
複製代碼

這樣配置以後,咱們就能夠給dispatch(這個dispatch並不是原始的dispatch)傳入一個函數,並能夠在該函數中使用異步數據流。markdown

thunk中間件

thunk的源碼很簡單,也就10多行代碼架構

function createThunkMiddleware(extraArgument) {
  // 返回值是一個 thunk,它是一個函數
  return ({ dispatch, getState }) => (next) => (action) => {
    // thunk 若感知到 action 是一個函數,就會執行 action
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    // 若 action 不是一個函數,則不處理,直接放過
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製代碼

在這裏先簡單的分析一下它的源碼。首先這個方法返回是一個三重的函數,第一重接收的是一個dispatch(組合中間件出來的一個鏈)和getStatestore.getState獲取state數據的方法)的對象,第二重是nextstore.dispatch最原始的dispatch方法),第三重是action(這個方法是一個action對象或者是一個異步的函數)。app

這裏的內容可能一會兒沒法理解,不要緊,接下來咱們配合ReduxapplyMiddleware的方法流程,一步步來剖析Redux的中間件原理。

Redux的中間件原理

咱們先看看applyMiddleware的源碼。

// applyMiddlerware 會使用「...」運算符將入參收斂爲一個數組
export default function applyMiddleware(...middlewares) {
  // 它返回的是一個接收 createStore 爲入參的函數
  return createStore => (...args) => {
    // 首先調用 createStore,建立一個 store
    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.`
      )
    }

    // middlewareAPI 是中間件的入參
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 遍歷中間件數組,調用每一箇中間件,而且傳入 middlewareAPI 做爲入參,獲得目標函數數組 chain
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 改寫原有的 dispatch:將 chain 中的函數按照順序「組合」起來,調用最終組合出來的函數,傳入 dispatch 做爲入參
    dispatch = compose(...chain)(store.dispatch)

    // 返回一個新的 store 對象,這個 store 對象的 dispatch 已經被改寫過了
    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

由於applyMiddleware是在createStore當中使用的,因此咱們也須要看一部分的createStore源碼。

function createStore(reducer, preloadedState, enhancer) {
    // 這裏處理的是沒有設定初始狀態的狀況,也就是第一個參數和第二個參數都傳 function 的狀況
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        // 此時第二個參數會被認爲是 enhancer(中間件)
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    // 當 enhancer 不爲空時,便會將原來的 createStore 做爲參數傳入到 enhancer 中
    if (typeof enhancer !== 'undefined') {
        return enhancer(createStore)(reducer, preloadedState);
    }
    ......
}
複製代碼

createStore對於applyMiddleware的使用邏輯比較簡單其實主要就是一句話

return enhancer(createStore)(reducer, preloadedState);
複製代碼

咱們把這幾個參數,代入applyMiddleware中再去看。

// middlewares是傳入的中間件
export default function applyMiddleware(...middlewares) {
  // 它返回的是一個接收 createStore 爲入參的函數
  return createStore => (...args) => {
    ......
  }
}
複製代碼

applyMiddleware方法中的createStore,其實就是Redux中的createStore方法,而args則對應的是reducerpreloadedState,這兩個參數均爲createStore函數的約定入參。

咱們接着來看下面的內容

// 首先調用 createStore,建立一個 store
const store = createStore(...args)
// 用來防止在遍歷 middleware 時調用dispatch
let dispatch = () => {
  throw new Error(
    `Dispatching while constructing your middleware is not allowed. ` +
      `Other middleware would not be applied to this dispatch.`
  )
}

// middlewareAPI 是中間件的入參
const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
// 遍歷中間件數組,調用每一箇中間件,而且傳入 middlewareAPI 做爲入參,獲得目標函數數組 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
複製代碼

這裏的內容就是調用createStore建立一個store。而後建立一個middlewareAPI對象,遍歷middleware(中間件),並傳入middlewareAPI參數。這裏其實就是以前thunk第一重函數接收的dispatchgetState

而後看下一句

dispatch = compose(...chain)(store.dispatch)
複製代碼

這裏的compose(...chain)咱們下面的章節再看,這裏能夠先把這個方法當作以下

dispatch = middleware(middlewareAPI)(store.dispatch)
複製代碼

這裏正好對於thunk的第二重函數接收next參數。這裏要注意一點,dispatch已經被重寫。

return {
  ...store,
  dispatch
}
複製代碼

最後返回一個重寫過dispatch的對象,這個對象會被綁定到咱們的頁面上,好比

<Provider store={store}>
  <App />
</Provider>
複製代碼

而後咱們經過useDispatch拿到的dispatch其實就是thunk中間件改寫過的方法。咱們用這個dispatch去傳入action對象或者異步數據流函數,其實就是調用thunk的第三重函數

(action) => {
  // thunk 若感知到 action 是一個函數,就會執行 action
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }
  // 若 action 不是一個函數,則直接使用dispatch派發action
  return next(action);
};
複製代碼

注意一點,這裏的dispatch已是被改寫的dispatch = compose(...chain)(store.dispatch)

這樣,咱們就經過中間件實現了Redux的異步數據流。咱們就能夠給dispatch傳入一個函數,來異步的派發action

compose

compose是一個函數式編程中,很經常使用的工具方法。它的功能就是把多個函數組合起來。

redux中如有多箇中間件,那麼redux會結合它們被「安裝」的前後順序,依序調用這些中間件。因此,咱們須要使用compose方法把中間件函數組合起來。

注意:compose只是一個函數式編程的思路,實現方式有不少種,下面只是redux中實現的一種

port default function compose(...funcs) {
  // 處理數組爲空的邊界狀況
  if (funcs.length === 0) {
    return arg => arg
  }

  // 若只有一個函數,也就談不上組合,直接返回
  if (funcs.length === 1) {
    return funcs[0]
  }
  // 如有多個函數,那麼調用 reduce 方法來實現函數的組合
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose(f1, f2, f3, f4);
// => 會轉換成下面這種形式
(...args) =>  f1(f2(f3(f4(...args))));
複製代碼

多箇中間件嵌套的時候,這裏的思路可能會有點繞,我舉個例子來講明一下。假設咱們使用了兩個中間件,一個是thunk,一個是redux-logger(當redux數據變動的時候,會打印相關信息到控制檯)。源碼地址

// dispatch定義
dispatch = thunk(createLogger(store.dispatch));
// dispatch被使用
dispatch(action);
//等同於
thunk(createLogger(store.dispatch))(action)
複製代碼

咱們來看下redux-logger的源碼,若是我只保留與redux中間件相關的邏輯的話,它的源碼能夠壓縮成幾行代碼。

return ({ getState }) => next => (action) => {
  return next(action);
};
複製代碼

這裏也有三重的代碼,第一重在咱們遍歷中間件數組的時候就被調用了

const chain = middlewares.map(middleware => middleware(middlewareAPI));
複製代碼

因此這裏傳入thunk的應該是第二重的代碼

thunk(createLogger(store.dispatch))(action)
// 等同於
thunk((action) => store.dispatch(action))(action)
複製代碼

咱們再來看看以前thunk中間件的源碼

function createThunkMiddleware(extraArgument) {
  // 返回值是一個 thunk,它是一個函數
  return ({ dispatch, getState }) => (next) => (action) => {
    // thunk 若感知到 action 是一個函數,就會執行 action
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    // 若 action 不是一個函數,則不處理,直接放過
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製代碼

咱們再次來精簡代碼

thunk((action) => store.dispatch(action))(action)
// 等同於
const thunk = (next) => (action) => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }
  return next(action);
};
thunk((action) => store.dispatch(action))(action);
// 等同於
const thunk = (action) => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }
  return store.dispatch(action);
};
thunk(action);
複製代碼

這樣,就實現了多箇中間件的依次調用。

感謝

若是本文對你有所幫助,請幫忙點個贊,感謝!

相關文章
相關標籤/搜索