從原始碼理解Redux和Koa2的中介軟體機制

jiangqizheng發表於2018-05-15

Redux和Koa的中介軟體機制相關原始碼都很精簡。

正文我將直接引用部分原始碼,並加以註釋來幫助我們更清晰的理解中介軟體機制。

Reudx

redux的中介軟體機制在原始碼中主要涉及兩個模組

內部的compose組合函式

'redux/src/compose.js'

//compose組合函式,接收一組函式引數返回一個組合函式
//需要提前注意的一點是,funcs陣列內的函式基本上(被注入了api)就是我們在未來新增的中介軟體如logger,thunk`等
export default function compose(...funcs) {
//為了保證輸出的一致性,始終返回一個函式
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
 //這一步可能有些抽象,但程式碼極其精緻,通過歸約函式處理陣列,最終返回一個逐層自呼叫的組合函式。
 //例: compose(f, g, h) 返回 (...args) => f(g(h(...args))).
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

//或許是版本更新的緣故,相比之前看到過的compose要精簡了很多,尤其是在最終的規約函式處理上,高大上了不少。
//由原本的reduce來依次執行中介軟體進化為函式自呼叫,更加的【函式式】。。下面順便貼出可能是舊的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)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
複製程式碼

我們新增中介軟體時最常用到的redux提供的applyMiddleware函式

'redux/src/applyMiddleware.js'

//可以看到compose函式被作為?引入了
import compose from './compose'
//暴露給開發者,用來傳入中介軟體
export default function applyMiddleware(...middlewares) {
    //createStore函式的控制權將被轉讓給applyMiddleware,由於本篇主要談中介軟體,就不擴充套件來解釋了
  return createStore => (...args) => {
//------------------------------------------------------------------非中介軟體相關,一些上下文環境的程式碼-----------------------
    //初始化store,此處的...args實際為reducer, preloadedState(可選)
    const store = createStore(...args)
    //宣告一個零時的dispatch函式,注意這裡的let,它將在構建完畢後被替換
    let dispatch = () => {
     throw new Error('dispatch不允許在構建中介軟體的時候被呼叫,其實主要是為了防止使用者自定義的中介軟體在初始化的時候呼叫dispatch。
                        在下文的示例中可以看到, 並且普通的同步的中介軟體一般是用不到dispatch的')
    }
    //提供給中介軟體函式的api,可以看到dispatch函式在這裡通過函式來'動態的呼叫當前環境下的dispatch'
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    //為中介軟體注入api
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
//------------------------------------------------------------------------------------------------------------------------
    
    '關鍵'
    //列出上文的例子可能比較直觀: 呼叫compose(f, g, h)返回(...args) => f(g(h(...args))).
    //1.呼叫compose函式,返回一個由多個/一箇中介軟體構成的組合函式
    //2.將store.dispatch作為引數傳入組合函式中用來返回一個新的/包裝過的dispatch函式
    //'注意:這部分需要聯絡下文中的中介軟體原始碼來對照著進行理解,所以讓我們暫時把這裡加入腦內快取'
    dispatch = compose(...chain)(store.dispatch)
    
    //返回一個store物件,在新增了中介軟體的情況下,我們實際最終獲取的store就是從這裡拿到的。
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

第三方中介軟體

本來不想寫這麼長來著,但希望更多大家能夠更簡單的理解,就多貼了些原始碼,畢竟程式碼遠比文字更好理解,下面我用logger和thunk的原始碼(簡化)來做承接上文的簡要分析。

'redux-logger'
//由於logger原始碼看起來好像有點複雜(懶得看),我就簡單實現了...不嚴謹請輕噴

通常來說redux的中介軟體主要分為兩層。

//第一層,用於接受store提供的API,在傳給構建中介軟體之前就會被呼叫。
const logger = ({getState}) => {
    // 第二層,利用了函式(currying)柯理化將計算/執行延遲,請讓我用更多的註釋來幫我們理清思路...
    // 還是先列出上文的例子比較直觀【手動滑稽】:)
    // 例:compose(f, g, h)返回(...args) => f(g(h(...args))). 
    // 關聯上文:dispatch = compose(...chain)(store.dispatch) 代入例子 ((dispatch) => f(g(h(dispatch))(store.dispatch)
    // 可以看到清晰看到,中介軟體被自右向左執行,store.dispatch作為引數被傳入給最先執行(最右側)的中介軟體
    // 中介軟體的第二層被執行,返回一個'接受action作為引數的函式',這個函式作為呼叫下一個(自己左側)的中介軟體,依次執行至最左側,最終返回的同樣是一個'接受action的函式'
    // 最終我們呼叫的dispatch實際上就是這個被最終返回的函式
    // '我們的真實流程是 dispatch(包裝過的) => 中介軟體1 => 中介軟體2 => dispatch(store提供的) => 中介軟體2 => 中介軟體1 => 賦值(如果有返回的話)'
    // 果然還是沒有解釋清楚,請拋開我的註釋,多看幾遍程式碼
    return next => action => {
        console.log('action', action)
        console.log('pre state', getState())
        //next實質就是下一個(右側)中介軟體返回的閉包函式/當前中介軟體如果是最後一個或者唯一的,那麼next就是store提供的dispatch
        //next(action)函式呼叫棧繼續往下走,也就是呼叫下一個(右側)中介軟體,nextVal會接受返回的結果
        const nextVal = next(action)
        console.log('next state', getState())
        //將結果返回給上一個中介軟體(左側)或者是開發者(第一個中介軟體的情況下)
        return nextVal
    }
}


'redux-thunk'
//這個是官方的原始碼,異常精簡,這個函式支援了dispatch的非同步操作,讓我們來看看如何實現的。
//這裡就不復述上面的註釋了,只解釋下關於非同步的支援。
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        //將dispatch函式的執行許可權轉移給開發者,我們通常在非同步結束之後呼叫dispatch(此時是同步的)。
        //注意:在這裡我們原本的中介軟體執行流程被中斷,並重新以同步的模式執行了一遍,'所以redux-thunk在中介軟體中的位置將會對其餘中介軟體造成影響,例如logger中介軟體被執行了兩次什麼的...'
        // 另一個要注意的是,這裡的dispatch函式實際上是在構築中介軟體後被包裝的函式。
        return action(dispatch, getState, extraArgument);
    }
    //dispatch同步時,直接將控制權轉讓給下一個中介軟體。
    //dispatch非同步時,在非同步結束後呼叫的dispatch中,同樣將控制權轉讓給下一個中介軟體。
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

複製程式碼

小結

最後讓我們梳理總結一下Redux的中介軟體流程。

  1. 首先是提供一個compsoe函式用來生成一個由多箇中介軟體構成的組合函式(存在自呼叫能力)
  2. 將store的API注入中介軟體
  3. 將store.dispath作為引數傳遞給一個由compose函式構成的組合函式,返回一個包裝後的dispatch(也就是我們真實使用的dispatch)
  4. (或者是0.)構築一個特定結構的的中介軟體,第一層用於注入API,第二層用來接受上一個中介軟體返回的一個接受action作為引數的函式,並且自身同樣返回一個包含中介軟體具體操作的接受action作為引數的函式
  5. 由中介軟體提供的dispatch被呼叫,中介軟體被依次呼叫,如果遇到提供了非同步支援,那麼在非同步情況下,dispatch會先按照普通流程呼叫,當遇到redux-thunk或者redux-promise等函式時,會以同步的形式重新呼叫當前dispatch(中介軟體也會被重新呼叫一遍)

下面丟張費了九牛二虎之力畫的呼叫流程圖...隨意看看就好...

從原始碼理解Redux和Koa2的中介軟體機制

Koa2

感覺基本沒多少朋友看到這裡了吧...但我還是要寫完。 同上,先貼原始碼讓程式碼來告訴我們真相

在redux裡,中介軟體是作為一個附加的功能存在的,但在koa裡中介軟體是它最主要但機制。

koa的核心程式碼被分散在多個獨立的庫中,首先來看中介軟體機制核心的compose函式

'koa-compose'

//注意:'在函式中始終返回Promise,是由於koa2採用了async await語法糖形式'
//接受一箇中介軟體陣列
function compose (middleware) {
    返回一個處理函式,在Request請求的最後被呼叫,並傳入請求的相關引數
  return function (context, next) {
    let index = -1
    //執行並返回第一個中介軟體
    return dispatch(0)
    
    一個接受一個數字引數,用來依次呼叫中介軟體
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      //在只有一箇中介軟體的時候直接呼叫自身
      if (i === middleware.length) fn = next
      //中介軟體被執行完畢了,直接返回一個Promise
      if (!fn) return Promise.resolve()
      try {
        //將下一個中介軟體的函式呼叫包裹在next中,返回給當前的中介軟體
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        //監聽錯誤,並丟擲
        return Promise.reject(err)
      }
    }
  }
}

//相比較redux的中介軟體缺少了幾分函式式的精緻,但我依舊寫不出類似精簡的程式碼.jpg
複製程式碼

koa本體

'koa/lib/application.js'

'104-115行的use函式(簡化)'

//這是koa暴露給我們的use函式,相信大多數同學都不陌生
  use(fn) {
    //非常明瞭,就是將中介軟體新增入middleware陣列
    this.middleware.push(fn);
    return this;
  }

'125-136行的callback函式'
//callback函式將在koa.listen中被呼叫具體請自行檢視原始碼

  callback() {
    //呼叫compsoe
    const fn = compose(this.middleware);
    if (!this.listeners('error').length) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      //Request時被呼叫
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

複製程式碼

koa2小結

關於koa2的中介軟體機制我並沒有解釋多少,主要是由於相比redux中介軟體來說簡明許多,另一個原因主要是懶,具體的執行流程圖,實際上同樣是洋蔥形的,只是store.dispatch被換成了最後一箇中介軟體而已。

本篇文章,雖然質量不行,大多註釋偏口語化(專業詞彙量不足),但還是希望能夠對一些同學有所幫助。

臨淵羨魚不如退而結網

相關文章