Redux Middleware中介軟體原始碼 分析

LIN . JY發表於2018-03-02

作者從2016年開始接觸 React+Redux,通過閱讀Redux原始碼,瞭解了其實現原理。

Redux程式碼量不多,結構也很清晰,函數語言程式設計思想貫穿著整個Redux原始碼,如純函式,高階函式,Curry,Compose。

本文首先會介紹函數語言程式設計的思想,再逐步介紹Redux中介軟體的實現。

看完本文,希望可以幫助你瞭解中介軟體實現的原理。

1) 基本概念

Redux是可預測的狀態管理框架。它很好的解決多互動,多資料來源的訴求。

Redux設計理念有三個原則: 1. 單一資料來源 2. State只讀 3. 使用純函式變更state值。

基本概念 原則 解釋
Store (1) 單一資料來源 (2) State只讀 Store可以看做是資料儲存的一個容器。在這個容器裡面,只會維護唯一的一個State Tree。

Store會給定4種基礎操作方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener)

根據單一資料來源原則,所有資料會通過store.getState()方法呼叫獲取。

根據State只讀原則,資料變更會通過store,dispatch(action)方法。

Action (3) 使用純函式變更state值 Action可以理解為變更資料的資訊載體。type是變更資料的唯一標誌,payload是用來攜帶需要變更的資料。

格式為:const action = { type: 'xxx', payload: 'yyy' };

Reducer (3) 使用純函式變更state值 Reducer是個純函式。負責根據獲取action.type的內容,計算state數值。

reducer: prevState => action => newState。

正常的一個同步資料流為:view層觸發actionCreator,actionCreator通過store.dispatch(action)方法, 變更reducer。

但是面對多種多樣的業務場景,同步資料流方式顯然無法滿足。對於改變reducer的非同步資料操作,就需要用到中介軟體的概念。如圖所示。

Redux Middleware中介軟體原始碼 分析

2) 函數語言程式設計

函數語言程式設計貫穿著Redux的核心。這裡會簡單介紹幾個基本概念。如果你已經瞭解了函數語言程式設計的核心技術,例如 高階函式,compose, currying,遞迴,可以直接繞過這裡。

我簡單理解的函數語言程式設計思想是: 通過函式的拆解,抽象,組合的方式去程式設計。複雜問題可以拆解成小粒度函式,最終利用組合函式的呼叫達成目的。

2.1) 高階函式

Higher order functions can take functions as parameters and return functions as return values.

接受函式作為引數傳入,並能返回封裝後函式。

2.2) Compose

Composes functions from right to left.

組合函式,將函式串聯起來執行。就像domino一樣,推倒第一個函式,其他函式也跟著執行。

首先我們看一個簡單的例子。


// 實現公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
 
// 深度巢狀函式模式 deeply nested function,將所有函式串聯執行起來。
subtract(multiple(add(200)));
複製程式碼

上述例子執行結果為:500

compose 其實是通過reduce()方法,實現將所有函式的串聯。不直接使用深度巢狀函式模式,增強了程式碼可讀性。不要把它想的很難。


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(subtract, multiple, add)(200);複製程式碼

2.3) Currying

Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

翻譯過來是:把接受多個引數 的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

直接擼程式碼解釋

// 實現公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
  
// Curring實現 使用一層層包裹的單參匿名函式,來實現多引數函式的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);複製程式碼

*Currying只允許接受單引數。

3) Redux applyMiddleware.js

Redux中reducer更關注的是資料邏輯轉化,所以Redux中介軟體是為了增強dispatch方法出現的。如我們上面圖,所描述的流程。中介軟體呼叫鏈,會在dispatch(action)方法之前呼叫。

所以Redux中介軟體實現核心目標是:改造dispatch方法。

redux對中介軟體的實現,程式碼是很精簡。整體都不超過20行。


export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []
 
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
 
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

接下來,一步步的解析Redux在中介軟體實現的過程。

Redux Middleware中介軟體原始碼 分析

applyMiddleware.js 方法有三個主要的步驟,如下:

  1. 將所有的中介軟體組合在一起, 並保證最後一個執行的是dispatch(action)方法。
  2. 像Koa所有中介軟體對ctx的呼叫一樣。保證所有的中介軟體都能訪問到Store。
  3. 最後將含有中介軟體呼叫鏈的新dispatch方法,合併到Store中。
  4. redux對中介軟體的定義格式為:mid1 = store => next => action => { next(action) };

看到這裡,你可能有這麼幾個疑問?

  1. 如何將所有的middleware串聯執行在一起?並可以保證最後一個執行的是dispatch方法?
  2. 如何讓所有的中介軟體都可以訪問到Store?
  3. 因為新形成的dispatch方法,為含有中介軟體呼叫鏈的方法結合。中介軟體如果呼叫dispatch,豈不是會死迴圈在呼叫鏈中?
  4. 為什麼將中介軟體格式定義為 mid1 = store => next => action => { next(action) } ?

為了解決這4個疑問,下面將針對相應問題,逐步解析。

3.1) 中介軟體串聯

疑問:

  1. 如何將所有的middleware串聯執行在一起?並可以保證最後一個執行的是dispatch(action)方法?

解決思路:

  1. 深度巢狀函式 / compose組合函式方法,將所有的中介軟體串聯起來。
  2. 封裝最後一個函式作為dispatch(action)方法。

const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
  1. compose(...)將所有中介軟體串聯
  2. 定義final作為最後執行dispatch的函式
*/
compose(final, middleware2, middleware1)(action)
複製程式碼

3.2) 中介軟體可訪問Store

疑問:

  1. 如何讓所有的中介軟體都可以訪問到Store?

可以參考我們對Koa2中介軟體的定義 const koaMiddleware = async (ctx, next) => { };

解決思路:

  • 給每一個middleware傳遞Store, 保證每一箇中介軟體訪問到的都是一致的。

const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);
複製程式碼

如果我們想使用compose方法,將所有中介軟體串聯起來,那就必須傳遞單一引數。

根據上面函數語言程式設計講到的currying方法,對每個中介軟體柯里化處理。


// 柯里化處理引數
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
 
// 將store儲存在各個函式中 -> 迴圈執行處理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);
複製程式碼

通過迴圈處理,將store內容,傳遞給所有中介軟體。這裡就體現了currying的作用,延遲計算和引數複用。

3.3) 中介軟體呼叫新dispatch方法死迴圈

疑問:

  1. 因為新形成的dispatch方法,為含有中介軟體呼叫鏈的方法結合。中介軟體如果呼叫dispatch,豈不是會死迴圈在呼叫鏈中?

new_dispatch = compose(...chain)(store.dispatch);   

new_store = { ...store, dispatch: new_dispatch };
複製程式碼

根據原始碼的解析,新和成new_dispatch是帶有中介軟體呼叫鏈的新函式,並不是原來使用的store.dispatch方法。

如果根據3.2) 例子使用的方式傳入store, const chain = [final, middleware2, middleware1].map(midItem => midItem(store));

此時儲存在各個中介軟體中的store.dispatch為已組合中介軟體dispatch方法,中介軟體如果呼叫dispatch方法,會發生死迴圈在呼叫鏈中。

根據上述文字的描述,右圖是死迴圈的說明。

Redux Middleware中介軟體原始碼 分析

解決思路:

  1. 給定所有中介軟體的dispatch方法為原生store.dispatch方法,不是新和成的dispatch方法。

// 這就是為什麼在給所有middleware,共享Store的時候,會重新定義一遍getState和dispatch方法。

const middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => dispatch(action)
}

chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
複製程式碼

3.4) 保證中介軟體不斷裂

疑問:

  1. 為什麼將中介軟體格式定義為 mid1 = store => next => action => { next(action) } ?

上述例子有提到每次都會返回action給下一個中介軟體,例如 const middleware1 = store => action => action;

如何保證中介軟體不會因為沒有傳遞action而斷裂?

這裡必須說明的是:Koa中介軟體可以通過呼叫await next()方法,繼續執行下一個中介軟體,也可以中斷當前執行,比如 ctx.response.body = ‘xxxx’ (直接中斷下面中介軟體的執行)。

一般情況下,Redux不允許呼叫鏈中斷,因為我們最終需要改變state內容。(* 比如redux-thunk使用有意截斷的除外)。

解決思路:

  1. 如果可以保證,上一個中介軟體都有下一個中介軟體的註冊,類似Koa對下一個中介軟體呼叫方式next(),不就可以保證了中介軟體不會斷裂。

// 柯里化處理引數
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
 
// 中介軟體串聯
const chain = [middleware1, middleware2 ].map(midItem => midItem({
  dispatch: (action) => store.dispatch(action)}));
 
// compose(...chain)會形成一個呼叫鏈, next指代下一個函式的註冊, 如果執行到了最後next就是原生的store.dispatch方法
dispatch = compose(...chain)(store.dispatch);
複製程式碼

Redux Middleware中介軟體原始碼 分析

4) 總結

Redux applyMiddleware.js機制的核心在於,函數語言程式設計的compose組合函式,需將所有的中介軟體串聯起來。

為了配合compose對單參函式的使用,對每個中介軟體採用currying的設計。同時,利用閉包原理做到每個中介軟體共享Store。

另外,Redux / React應用函數語言程式設計思想設計,其實是通過組合和抽象來減低軟體管理複雜度。

簡單寫了個學習例子 參考 https://github.com/Linjiayu6/learn-redux-code, 如果有幫助到你,點個贊 咩~

簡歷請投遞至郵箱linjiayu@meituan.com

相關文章