昨天的文章手寫了一版redux的核心源碼,redux庫除了數據的狀態管理還有一塊重要的內容那就是中間件,今天我仍是嘗試將此部分源碼完成。javascript
react中管理數據的流程是單向的,就是說,從派發動做一直到發佈訂閱觸發渲染是一條路走到頭,那麼若是想要在中間添加或是更改某個邏輯就須要找到action或是reducer來修改,有沒有更方便的作法呢?java
而中間件(middleware)就是一個可插拔的機制,若是想要擴展某個功能,好比添加日誌,在更新先後打印出state狀態,只須要將日誌中間件裝到redux上便可,因而便有了日誌功能,當不想使用時可再拿掉,很是方便。react
目前有不少第三方的中間件安裝便可使用,好比剛剛說起的日誌中間件:redux-logger,使用npm安裝它:面試
npm install redux-logger
npm
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更新日誌:redux
以上就是一個使用中間件的例子。app
那麼中間件的執行原理是什麼呢?就用剛剛的日誌中間件舉例,它的功能是在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);
中間件原理咱們分析完了,即然中間件就是擴展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庫嗎?人家的applyMiddlewares方法是支持傳入多箇中間件的,如:applyMiddlewares(middleware1,middleware2);
咱們目前的方法還不支持這種寫法,最終的目的是想把若干個中間件一次組合爲一個總體,一塊兒加載。
洋蔥模型的概念彷佛是在Koa2框架中提出的,它是指中間件的執行機制,當多箇中間件執行時,後一箇中間件會套在前一箇中間件的裏面:
執行完一箇中間件會一直向裏走,直到最後一個執行結束,再從內而外走出,就像是在剝洋蔥同樣。
咱們一樣使用洋蔥模型來寫一個組合方法,以達到目的。
新建一個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庫的一個難點,主要是層層調用關係很是惱人,一個好辦法是經過庫源碼與中間件源碼對比來分析,理清思路便可,若是還有時間我會嘗試再手寫一版react-redux庫,一個是學習提升,二是應付面試。