Redux系列之分析中間件原理(附經驗分享)

前提:公司兼容了兩種技術棧VueReactVue研究的比較多一些,反觀React還停留在查官方文檔階段,最近恰好維護了一個React項目,項目中用到Redux,藉此從新複習Reduxjavascript

Redux

一句話介紹Redux:Redux是一個可預測化的JavaScript狀態管理容器。html

三大原則

理解Redux離不開這三大原則java

單一數據源

整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。git

State 是隻讀的

惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象。github

store.dispatch({
  type'COMPLETE_TODO',
  index1
})
複製代碼

使用純函數來執行修改

爲了描述 action 如何改變 state tree ,你須要編寫 reducers。action是描述修改操做,而真正去操做修改state是reducersweb

function reducer(state = [], action{
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completedfalse
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completedtrue
          })
        }
        return todo
      })
    default:
      return state
  }
}
複製代碼

經過上面👆的分析咱們獲得三個關鍵的信息點:state,action,reducersredux

概念

在複習工做流以前咱們先來搞清楚幾個概念,如下的概念解釋都出自於詞彙表 , 爲了方便閱讀,我就摘抄過來了promise

State

State (也稱爲 state tree) 是一個寬泛的概念,可是在 Redux API 中,一般是指一個惟一的 state 值,由 store 管理且由 getState() 方法得到。它表示了 Redux 應用的所有狀態,一般爲一個多層嵌套的對象。網絡

約定俗成

頂層 state 或爲一個對象,或像 Map 那樣的鍵-值集合,也能夠是任意的數據類型。然而你應儘量確保 state 能夠被序列化,並且不要把什麼數據都放進去,致使沒法輕鬆地把 state 轉換成 JSON。app

Action

Action 是一個普通對象,用來表示即將改變 state 的意圖。它是將數據放入 store 的惟一途徑。不管是從 UI 事件、網絡回調,仍是其餘諸如 WebSocket 之類的數據源所得到的數據,最終都會被 dispatch 成 action。

約定俗成

action 必須擁有一個 type 域,它指明瞭須要被執行的 action type。Type 能夠被定義爲常量,而後從其餘 module 導入。比起用 Symbols 表示 type,使用 String 是更好的方法,由於 string 能夠被序列化。

Reducer

Reducer (也稱爲 reducing function) 函數接受兩個參數:以前累積運算的結果和當前被累積的值,返回的是一個新的累積結果。該函數把一個集合歸併成一個單值。

在 Redux 中,累計運算的結果是 state 對象,而被累積的值是 action。Reducer 由上次累積的結果 state 與當前被累積的 action 計算獲得一個新 state。這些 Reducer 必須是純函數,並且當輸入相同時返回的結果也會相同。它們不該該產生任何反作用。正因如此,才使得諸如熱重載和時間旅行這些很棒的功能成爲可能。

dispatch 函數

dispatching function(或簡言之 dispatch function) 是一個接收 action 或者異步 action的函數,該函數要麼往 store 分發一個或多個 action,要麼不分發任何 action。

Action Creator

Action Creator 很簡單,就是一個建立 action 的函數。不要混淆 action 和 action creator 這兩個概念。Action 是一個信息的負載,而 action creator 是一個建立 action 的工廠

異步 Action

異步 action 是一個發給 dispatching 函數的值,可是這個值還不能被 reducer 消費。在發往 base dispatch() function 以前,middleware 會把異步 action 轉換成一個或一組 action。異步 action 能夠有多種 type這取決於你所使用的 middleware。它一般是 Promise 或者 thunk 之類的異步原生數據類型雖然不會當即把數據傳遞給 reducer,可是一旦操做完成就會觸發 action 的分發事件

Middleware

Middleware 是一個組合 dispatch function 的高階函數返回一個新的 dispatch function,一般將異步 actions 轉換成 action。

❗️❗️這也是接下來咱們要重點分析

最後放出一張記憶腦圖

Redux工做流

瞭解了三大原則以及概念之後,來看看Redux的工做流吧

對比下圖就能輕鬆理解了

從圖中咱們知道Redux是單向數據流,那麼根據上面所學的知識咱們來設計下咱們的Redux目錄結構

如上圖,大部分公司的Redux目錄結構應該相似這樣,咱們須要actionCreators文件來建立咱們的actionaction對象必須擁有一個 type 域,而後reducer根據不一樣的type觸發對應的操做,因此建立actionTypes文件,接下來就是處理reducer了來修改咱們的state,因此建立reducer文件📃,因此明白上述概念,有助於咱們對目錄結構的理解,而不是傻乎乎的跟着別人的目錄結構照貓畫虎的建立,起碼要明白爲何這樣劃分

compose聚合函數

再看中間件原理時,咱們來實現下compose函數,理解它對於理解咱們中間件原理有很大幫助

dispatch=fn1(fn2(fn3))
dispatch=compose(fn1,fn2,fn3)
複製代碼

咱們期待有一個聚合的方法compose,能夠這樣使用,參數從右至左,將第一個參數fn3做爲第二個參數fn2的參數,並將運行結果做爲第三個參數fn1的參數,依次遞推,最終返回一個新的函數,這個新函數在基礎函數f3的基礎上,獲得了全部的高階函數的能力,🤔思考:假設這個f3參數是換成是dispatch函數呢?,不着急,咱們接着往下分析compose函數的實現

// compose聚合函數的順序是從右到左 from right to left
const compose = function (...funcs{
    return funcs.reduce((a, b) => {
        return (...args) => {
           return a(b(...args))
        }
    })
}
複製代碼

這些串聯函數不優雅。ES6 的箭頭函數簡寫 ,從而看起來更舒服一些

 function compose(...funcs{
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼

因此compose(fn1, fn2, fn3) (...args) 至關於 fn1(fn2(fn3(...args)))

Redux中間件原理

講清楚了上述👆基本內容後,到了本問的關鍵點了,那就是Redux中間件是用來幹嗎的,原理是什麼❓

前面分析Redux工做流Action發出之後,Reducer 當即算出 State,是個同步流程,那麼想一想如何支持異步操做,不僅僅支持異步操做,還要支持錯誤處理、日誌監控,那麼是在Redux工做流哪一個環節進行攔截操做呢❓,答案是dispatch過程,在分發action進行攔截處理

在Redux中,與中間件的實現相關聯的方法是applyMiddleware,因此咱們來分析下這個方法吧(這裏筆者提供一份調試中間件代碼點擊進入倉庫

// 調用applyMiddleware
applyMiddleware(thunk, logger)

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) => {
        return dispatch(...args)
      }
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

能夠看到dispatch = compose(...chain)(store.dispatch)這行關鍵代碼,Redux爲了支持中間件,內部重寫了原來的dispatch方法,同時最後傳入原來的store.dispatch方法,也就是將原來的disptach方法也當作中間件處理了

分析到這裏,咱們能夠知道傳入的中間件compose函數聚合後改寫dispatch方法,因此咱們也能夠理解成中間件是對dispatch方法的加強,好比說:加強dispatch函數對action是函數、Promise的處理

舉個例子🌰

倉庫內修改爲這段代碼

function logger(store{
    return function wrapDispatchToAddLogging(next{
        return function dispatchAndLog(action{
            let result = next(action)
            return result
        }
    }
}

function thunk({ dispatch, getState }{
    return function wrapDispatchToThunk(next{
        return function dispatchThunk(action{
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
        }
    }
}

applyMiddleware(thunk, logger)  // 至關於 wrapDispatchToThunk(wrapDispatchToAddLogging(dispatch))  
複製代碼

打印dispatch方法

dispatchThunk(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
    }
複製代碼

轉換next

// 轉換next
dispatchThunk(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            // next(action)
            // 這裏的next由來是執行logger方法返回了dispatchAndLog函數
            return (function dispatchAndLog(action{
            let result = next(action)
            return result
        })(action)
   }   
複製代碼

繼續轉換next

看到dispatchAndLog函數裏還有個next,咱們繼續轉換

// 轉換next
dispatchThunk(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            // next(action)
            // 這裏的next由來是執行logger方法返回了dispatchAndLog函數
            return (function dispatchAndLog(action{
            // let result = next(action)
           // 這裏的dispatch是原來的dispacth
            let result=(function(action){dispatch(action)})(action)             
            return result
        })(action)
   }   
複製代碼

看到這裏咱們已經知道,Redux實現中間件的原理核心是加強原來dispatch函數的能力,然函數擁有某種能力天然而然想到高階函數的處理方式,compose 方法將新的 middlewares 和 store.dispatch 結合起來,生成一個新的 dispatch 方法,另外經過改寫後的dispatch方法,能夠肯定Redux中間件也是基於洋蔥模型

中間件的執行順序

執行順序聽從洋蔥模型

  applyMiddleware( 
    logger,
    thunk
  )
複製代碼

如何寫Redux中間件

工做中咱們明白了Redux工做流的狀況下,其實幹擾最多的可能就是Redux中間件了,經常使用的有redux-thunk、redux-soga、redux-promise等等,因此掌握中間件原理仍是很重要的,那麼如何去寫一箇中間件❓,咱們經過繼續分析applyMiddleware方法

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) => {
        return dispatch(...args)
      }
    }
    // 這裏執行了一層中間件接收了{store.getState,dispatch}參數
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose(...chain)(store.dispatch) 至關於fn1(fn2(fn3(store.dispatch)))
    // 又執行了一層中間件 這一層接收next參數 也就是下一個中間件參數
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

經過上面👆的分析可知,一個Redux中間件的基本形式(結構)以下

// 中間件邏輯代碼須要通過三次柯里化
store => next => action => {
  // 中間件邏輯代碼
}
複製代碼

Redux支持異步操做

  • 強化dispacth函數,讓其能解析action爲函數形式,從而讓Redux支持異步操做
  • 強化dispacth函數,讓其能解析action爲Promise對象形式,從而讓Redux支持異步操做
redux-thunk

根據這個中間件結構咱們來分析redux-thunk中間件的源碼

function createThunkMiddleware(extraArgument{
  return ({ dispatch, getState }) => (next) => (action) => {
    // 若是是函數 thunk來處理
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    // 其它處理不了,交給下一個中間件處理
    return next(action);
  };
}
const thunk = createThunkMiddleware();
複製代碼

嗯嗯…,redux-thunk就這麼幾行源代碼就實現了支持Redux異步操做

redux-promise

用法

  const testRes = () => {
      return new Promise((res, rej) => {
          res({
              type'TEST_RESOLVE'
          })
      });
  }
  store.dispatch(testRes());
複製代碼
// redux-promise簡易版源碼
const vanillaPromise = store => next => action => {
  // 判斷不是Promise對象,交給下箇中間件處理
  if (typeof action.then !== 'function') {
    return next(action)
  }
  // action爲Promise對象,promise中間件能作處理
  // 最後異步執行完觸發執行store.dispatch ---> (...args) =>  dispatch(...args)

  return Promise.resolve(action).then(store.dispatch)
}
複製代碼

改造咱們的Redux項目

一句話Redux用起來太笨重了,去年實習期開始寫React項目時候,跟着別人的風格來寫而已,直到維護同事的React項目,才猛然意識到項目中糟糕Redux寫法,濫用Redux,致使了這個項目是災難級別的

後面意識到能不能二次封裝下Redux,簡化寫法

好比:

import { createModel } from "../../../model.js";

const model = {
  namespace'counter',
  state: {
    count10
  },
  reducer: {
    add(state: any, action: any) { // counter/add
      state.count += 1
    },
    minus(state: any, action: any) {
      state.count--
    },
  }
}

export default createModel(model)
複製代碼

enennene….就有了以上對話,也感謝大佬的幫助,最後選用了remacth方案,這個庫也挺好友好,兼容了老的寫法,具體能夠細看這個它的文檔,這裏就不作分析了

相關文章
相關標籤/搜索