面試還問redux?那我從頭手擼原始碼吧(中介軟體)

恍然小悟發表於2018-10-24

昨天的文章手寫了一版redux的核心原始碼,redux庫除了資料的狀態管理還有一塊重要的內容那就是中介軟體,今天我還是嘗試將此部分原始碼完成。

中介軟體

react中管理資料的流程是單向的,就是說,從派發動作一直到釋出訂閱觸發渲染是一條路走到頭,那麼如果想要在中間新增或是更改某個邏輯就需要找到action或是reducer來修改,有沒有更方便的做法呢?

而中介軟體(middleware)就是一個可插拔的機制,如果想要擴充套件某個功能,比如新增日誌,在更新前後列印出state狀態,只需要將日誌中介軟體裝到redux上即可,於是便有了日誌功能,當不想使用時可再拿掉,非常方便。

目前有很多第三方的中介軟體安裝即可使用,比如剛剛提及的日誌中介軟體:redux-logger,使用npm安裝它:

npm install redux-logger

redux包提供了一個方法可以裝載中介軟體:applyMiddleware。在建立store物件的時候,可以傳入第二個引數,它就是中介軟體:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
//使用applyMiddleware載入中介軟體
let store = createStore(reducer, applyMiddleware(ReduxLogger));
複製程式碼

裝載好中介軟體就在派發動作上擴充套件了相應的功能,這時我們正常編寫redux程式,當執行dispatch方法時會在控制檯列印出state更新日誌:

logger

以上就是一個使用中介軟體的例子。

淺析中介軟體的原理

那麼中介軟體的執行原理是什麼呢?就用剛剛的日誌中介軟體舉例,它的功能是在state物件的更新前後分別輸出狀態,那麼肯定是在派發(dispatch)動作的那一刻去實現的,那我們改寫一下redux庫,將“列印日誌”功能新增到dispatch方法裡:

let temp = store.dispatch;//暫存原dispatch方法
store.dispatch = function(action) {
  console.log("舊state:", store.getState());
  temp(action);//執行原dispatch方法
  console.log("新state:", store.getState());
};
複製程式碼

這樣就實現了“日誌中介軟體”,但是直接改寫redux庫是不可能的,我們需要一個通用的辦法去定義中介軟體,redux提供了這樣一個方法:applyMiddleware

它的使用方法很簡單,將需要載入的中介軟體依次傳入applyMiddleware方法中即可:

applyMiddleware(ReduxLogger, ReduxThunk);

手寫applyMiddleware原始碼

中介軟體原理我們分析完了,即然中介軟體就是擴充套件dispatch方法,那麼applyMiddlware必然會將中介軟體的dispatch方法和原始dispatch傳入才可行,沒錯,我們就看看它的方法簽名:

var applyMiddleware = (middlewares) => (createStore) => (reducer) => {};
複製程式碼

以上就是applyMiddleware方法,它又是一個三層的高階函式,這裡用到了函式柯里化的思想,將多個引數拆分為單一引數的高階函式,以保證每一層只有一個引數,這樣更加靈活可分塊呼叫。寫成箭頭函式不好理解,我們改寫為普通函式形式:

var applyMiddleware = function (middlewares){
  return function (createStore){
    return function (reducer){
      //在這裡裝載中介軟體
    }
  }
};
複製程式碼

通過函式引數就可以看到,三層函式分別傳入了中介軟體(middleware)、建立倉庫方法(createStore)和reducer函式,這正是我們裝載一箇中介軟體所需要的。

接下來我們的目標就是將中介軟體提供的dispatch覆蓋redux原有的dispatch方法,這樣就“裝載”好了中介軟體。

var applyMiddleware = function (middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            //呼叫中介軟體,返回新dispatch方法
            let newDispatch = middlewares(store)(store.dispatch);
            //覆蓋原有的dispatch方法並返回倉庫物件
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}
複製程式碼

有了通用寫法,我們自己模擬實現一個日誌中介軟體:

function reduxLogger(store) {
    return function (dispatch) {
        //dispatch引數即原redux派發方法
        return function (action) {
            //返回的這個函式即新方法
            //最終會傳入applyMiddleware覆蓋掉dispatch
            console.log(`更新前:${JSON.stringify(store.getState())}`);
            dispatch(action);
            console.log(`更新後:${JSON.stringify(store.getState())}`);
        }
    }
}
複製程式碼

呼叫我們自己的方法裝載中介軟體:applyMiddleware(reduxLogger);,執行效果如下:

面試還問redux?那我從頭手擼原始碼吧(中介軟體)

組合中介軟體

但是到現在還沒完,還記得官方redux庫嗎?人家的applyMiddlewares方法是支援傳入多箇中介軟體的,如:applyMiddlewares(middleware1,middleware2); 我們目前的方法還不支援這種寫法,最終的目的是想把若干個中介軟體一次組合為一個整體,一起載入。

洋蔥模型

洋蔥模型的概念似乎是在Koa2框架中提出的,它是指中介軟體的執行機制,當多箇中介軟體執行時,後一箇中介軟體會套在前一箇中介軟體的裡面:

面試還問redux?那我從頭手擼原始碼吧(中介軟體)

執行完一箇中介軟體會一直向裡走,直到最後一個執行結束,再從內而外走出,就像是在剝洋蔥一樣。

compose方法

我們同樣使用洋蔥模型來寫一個組合方法,以達到目的。

新建一個compose.js,建立一個組合函式:

/**
 * 組合所有中介軟體
 * @param  {...any} middlewares 
 */
function compose(...middlewares) {
    return function (...args) {

    }
}
複製程式碼

我的目標是當呼叫組合函式,傳入多箇中介軟體,將所有的中介軟體組合成一個函式:

var all = compose(middleware3, middleware2, middleware1);
all();//呼叫時,依次執行所有中介軟體
複製程式碼

我們動手實現它。

寫之前我們先想一下,組合功能即是將“若干”個功能封裝為“一個”功能,這正是函數語言程式設計的收斂思想,ES6中已經為我們提供了reduce函式,在這裡最合適不過了:

/**
 * 組合所有中介軟體
 * @param  {...any} middlewares 
 */
function compose(...middlewares) {
  //args即第一個中介軟體所需引數
  return function (...args) {
    return middlewares.reduce((composed, current) => {

      return function (...args) {
        //當前中介軟體的執行結果即上一個中介軟體的引數
        return composed(current(...args));
      }
    })(...args);
  }
}
複製程式碼

通過reduce函式,一步一步將後一箇中介軟體套到前一箇中介軟體之中,後一箇中介軟體的結果即前一個的引數,這樣層層遞近,最終返回一個大函式,即完成組合。

最後可以優化為箭頭函式的形式,顯得逼格更高一點:

function compose(...middlewares) {
  return (...args) => middlewares.reduce((composed, current) => (...args) => composed(current(...args)))(...args)
}
複製程式碼

完成中介軟體裝載

在compose完成之後,最後一步的工作就是改寫applyMiddlewares將所有傳入的中介軟體組合好:

function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            //一次傳入多箇中介軟體,迴圈包開一層函式
            let chain = middlewares.map(middleware => {
                return middleware(store);
            });

            //組合所有的中介軟體
            let newDispatch = compose(...chain)(store.dispatch);
            //覆蓋原有的dispatch方法
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}
複製程式碼

至此,redux庫的原始碼已經基本實現完畢。

多箇中介軟體執行如下:

面試還問redux?那我從頭手擼原始碼吧(中介軟體)

尾巴

這兩天從頭手寫了一遍redux庫發現redux的原始碼量並不大但是邏輯還是很複雜的,理清redux的流程是讀寫原始碼的前提。而中介軟體則是redux庫的一個難點,主要是層層呼叫關係非常惱人,一個好辦法是通過庫原始碼與中介軟體原始碼對比來分析,理清思路即可,如果還有時間我會嘗試再手寫一版react-redux庫,一個是學習提高,二是應付面試。

相關文章