中介軟體是個什麼東西?
redux中管理資料的流程是單向的,就是說,從派發動作一直到釋出訂閱觸發渲染是一條路走到頭,那麼如果想要在中間新增或是更改某個邏輯就需要找到action或是reducer來修改,有沒有更方便的做法呢?
redux的流程:
button -觸發事件-> dispath -派發動作-> reducer -釋出訂閱-> view
而中介軟體(middleware)就是一個可插拔的機制,如果想找擴充套件某個功能,比如新增日誌,在更新前後列印出state狀態,只需要將日誌中介軟體裝到redux上即可,於是便有了日誌功能,當不想使用時可再拿掉,非常方便。
中介軟體的使用
先說說用法,只有會用了,再說原理。
redux-logger
redux提供了好多個現成的中介軟體,比如上面提到的日誌中介軟體,安裝它即可使用:
npm i --save 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));
複製程式碼
這樣在每一次更新state,會在控制檯列印更新日誌:
redux-thunk
redux-thunk中介軟體可以支援非同步action。
載入中介軟體:
import reduxThunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(reduxThunk));
複製程式碼
當載入了redux-thunk中介軟體,action函式可以支援返回一個函式,將非同步操作封裝在裡面:
function add(payload) {
return function(dispatch, getState) {
setTimeout(() => {
dispatch({ type: ADD, payload });
}, 2000); //延時2秒執行
};
}
複製程式碼
可以看到action又返回一個函式,其中的引數dispatch和getState就是redux提供的方法,它將這兩個函式的使用權交給了我們,讓我們等待非同步操作完成 時再呼叫,完成非同步action編寫。
redux-promise
有了redux-thunk中介軟體我們可以編寫非同步action,但我們想更進一步,讓非同步action支援Promise,那麼redux-promise中介軟體可派上用場。
還是安裝redux-promise然後載入它:
import reduxPromise from "redux-promise";
let store = createStore(
reducer,
applyMiddleware(reduxPromise)
);
複製程式碼
redux-promise中介軟體可以支援action返回的物件payload為一個Promise:
let action = {
add: function(payload) {
return {
type: ADD,
//payload是一個Promise物件,非同步操作封裝到裡面
payload: new Promise((resolve, reject) => {
setTimeout(() => {
resolve(payload); //執行成功,將引數傳到reducer
}, 1000);
})
};
},
minus: function(payload) {
return {
type: MINUS,
payload: new Promise((resolve, reject) => {
setTimeout(() => {
reject(payload); //執行失敗,將引數傳到reducer
}, 1000);
})
};
}
};
複製程式碼
可以看到,payload不再是直接返回引數,而是改為一個Promise物件,這樣就可以把非同步程式碼封裝到裡面。
注意!如果你使用redux-promise中介軟體,payload引數名是固定的,不可隨意改名
比如:
{
type: MINUS,
num: new Promise((resolve, reject) => {
//...
})
};//此處引數名是num,redux-promise不能正確識別,若使用redux-promise必須叫payload
複製程式碼
中介軟體的原理
通過以上三個中介軟體,可以清楚了它們的用法,都是在state更新的前後擴充套件一些功能,那麼它們的原理是什麼呢?
拿第一個中介軟體redux-logger來舉例,日誌是列印在state更新的前後,那麼改寫store.dispatch()
方法是一個方案:
let temp = store.dispatch;//暫存原dispatch方法
store.dispatch = function(action) {
console.log("舊state:", store.getState());
temp(action);//執行原dispatch方法
console.log("新state:", store.getState());
};
複製程式碼
可以看到,首先將原來的dispatch方法臨時儲存到了變數中,並將現有的dispatch方法改寫,增加了輸出日誌的功能,在state未更新之前先輸出,再呼叫暫存的dispatch更新state即可,這樣就相當於實現了redux-logger中介軟體。
雖然這種寫法很噁心,但是這就是redux中介軟體的原理:暫存原dispatch方法,修改dispatch擴充套件功能並返回。
中介軟體的通用寫法
原理明白了,但是每次都手動去覆蓋dispath顯然太過麻煩,有沒有通用的寫法呢?顯然是有的。
redux原始碼中是使用高階函式去實現一箇中介軟體,它的方法籤明是這樣的:
let middleware = store => next => action => {
//具體中介軟體的邏輯...
};
複製程式碼
可以看到,箭頭函式的寫法非常優雅,它是一個三層巢狀的函式,也就是高階函式,它的最終返回值仍是一個方法,這個方法就是最終“擴充套件”了功能的“dispatch”方法。
不好理解?我們可以寫成普通函式的形式,更容易看清邏輯:
function middleware(store) {
//next為原dispatch方法
return function(next) {
//action為傳入派發器的action物件
return function(action) {
//中介軟體的具體邏輯寫在這兒...
};
};
}
複製程式碼
即然中介軟體就是改寫原dispath方法,那麼我們可以想一想,要想擴充套件原來的dispath都需要哪些東西?應該是以下這些:
- store倉庫物件(有了store物件才能覆蓋之前的dispath方法)
- dispatch方法(之前的dispatch)
- action物件 (派發動作需要action物件)
以上這三個物件必不可少,可以看到這三個物件正是三層函式的引數。第一層store引數實際是createStore()
的返回值,就是倉庫;第二層的next引數就是原dispatch方法;最內層的函式引數則是action物件。
搞清楚了方法簽名的結構,我們就可以自己寫出一個redux-logger中介軟體:
export function reduxLogger(store) {
//next為原dispatch方法
return function(next) {
//action為傳入派發器的action物件
return function(action) {
console.log("更新前:", store.getState());
next(action);
console.log("更新後:", store.getState());
};
};
}
複製程式碼
很簡單,在next()
執行的前後列印state的狀態即可。
applyMiddleware方法
我們手寫了一箇中介軟體,還要在需要時載入中介軟體,在redux中提供了一個applyMiddleware方法來載入中介軟體:
applyMiddleware(reduxLogger);
複製程式碼
將所需的中介軟體依次傳入即可載入中介軟體。那麼它的原理呢?不防也來看一看。
applyMiddleware的方法簽名仍是一個三層的高階函式,
let applyMiddleware = middlewares => createStore => reducer => {
//載入中介軟體的邏輯...
};
複製程式碼
還是一樣,我們改寫成普通函式來分析:
function applyMiddleware(middlewares) {
//createStore即redux提供的方法
return function(createStore) {
//reducer就是傳入更新state的函式
return function(reducer) {
//載入中介軟體的邏輯...
};
};
}
複製程式碼
想一下,在應用中介軟體的過程中,目的就是將外界傳入的中介軟體的新dispath方法覆蓋原有的store.dispatch
,這樣返回給使用者的store物件的dispatch方法已經由中介軟體擴充套件了,比如這裡就是列印日誌。
那麼applyMiddleware都需要哪些東西呢?
- 需要應用的中介軟體
- createStore方法(有了它就可以建立store物件)
- reducer(建立store物件時需要reducer引數)
可以看到這些正是三層高階函式的引數,這樣我們就可以寫出applyMiddleware的邏輯:
function applyMiddleware(middlewares) {
return function(createStore) {
return function(reducer) {
let store = createStore(reducer); //取得store物件
let dispatch = middlewares(store)(store.dispatch); //取得新的dispatch方法
return { ...store, dispatch }; //將新dispatch覆蓋舊的store.dispatch
};
};
}
複製程式碼
其中重點是這一條語句:
let dispatch = middlewares(store)(store.dispatch);
複製程式碼
這條語句就是取得中介軟體改寫後的dispatch方法,還記得中介軟體的簽名麼?不防對照它來看一下就會明白:
let middleware = store => next => action => {};
複製程式碼
中介軟體要求傳入第一個引數store物件,通過createStore(reducer)
建立;
中介軟體要求傳入第二個引數next,就是原dispatch,那麼store.dispatch就是原倉庫的dispatch。此時返回的結果就是新dispath方法了,最後使用展開運算子將原store物件上的dispatch覆蓋並返回即可。
到此,手寫中介軟體和應用中介軟體的全部原理已經分析完畢。