最近處在專案的間歇期,沒事參加了幾場面試發現面試官依然喜歡問redux的一些問題,尤其是問這種開發框架的問題最好的辦法就是撤底搞懂其原始碼,正好利用這兩天時間從頭過了一遍redux庫,還是有些收穫的。
redux原始碼我大致分了3塊,從易到難:
- 狀態管理核心程式碼
- react-redux庫
- 中介軟體
手寫原始碼不是目的,主要是為了看看大牛寫的程式碼更能開拓思維,以後和麵試官扯淡的時候能把他忽悠住。
下面從零開始,手擼一套自己的redux庫,預期與官方庫達到近似的功能,並且比較官方原始碼,看看自己的寫法有哪些不足。今天先從redux核心程式碼開始。
redux核心程式碼實現
動手之前先回顧一下redux是幹什麼的,它能解決什麼問題?redux的出現就是為了解決react元件的狀態管理。redux內部管理了一個狀態樹(state),根據開發者提供的reducer來“派發”一個“動作”以更新state,這樣資料管理全部交由redux來處理而不在由react元件去操心。其實redux只是一種資料管理的設計思想,而不是一個用於react中的特定框架,因此只要我們的業務足夠複雜,脫離react在任何環境下都能使用redux。
redux核心具有以下功能:
- 得到當前狀態(getState)
- 訂閱(subscribe)與退訂
- 派發動作以更新狀態(dispatch)
- 生成actionCreator
- 合併reducer
我們一一實現這些功能。
程式碼基本結構
redux的核心即狀態管理,一個資料倉儲中維護了一個狀態樹,我們要向開發者提供一個訪問狀態(state)的介面,我們寫出它的基本結構:
function createStore(reducer) {
var currentState; //狀態
var currentReducer = reducer; //外界提供的reducer
/**
* 暴露給開發者,得到當前狀態
*/
function getState() {
return currentState;
}
return {
getState
}
}
export {
createStore
}
複製程式碼
可以看到程式碼非常簡單,createStore函式接收一個reducer,因為具體更新state的邏輯是由開發者提供的,因此站在redux設計者的角度上,我只接收你給我的“邏輯”,而更新後的狀態封裝在內部currentState物件中,並提供一個訪問此物件的介面函式,這樣就通過閉包的方式保護好了內部的狀態。
派發功能的實現
redux架構中更新狀態的方式只有一個,那就是派發(dispatch)一個動作(action),不可以由開發者手動修改內部state物件,因此我們還要提供一個dispatch方法,使其具有更新狀態的功能。
function createStore(reducer) {
var currentState; //狀態
var currentReducer = reducer; //外界提供的reducer
/**
* 派發動作
* @param {Object} action Action物件
*/
function dispatch(action) {
currentState = currentReducer(currentState, action);
}
//其他程式碼略...
}
複製程式碼
以上就實現了派發功能,只此一條語句,呼叫開發者提供的reducer函式,並傳入action動作物件,即將更新後的新state覆蓋了舊物件。
但是隻此一條語句顯然不夠嚴謹,我們把程式碼寫得更健壯一些,如果傳入的action物件不合法(比如沒有type屬性)我們的程式碼是會出現錯誤。
function createStore(reducer) {
var currentState;
var currentReducer = reducer;
var isDispatching = true; //正在派發標記
/**
* 派發動作
* @param {Object} action Action物件
*/
function dispatch(action) {
//驗證action物件合法性
if (typeof action.type === `undefined`) {
throw new Error(`Action 不合法`);
}
if (isDispatching) {
throw new Error(`當前狀態正在分發...`);
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
}
//其他程式碼略...
}
複製程式碼
官方原始碼中還加入了一個“正在派發”的標誌,若當前redux呼叫棧正處於派發當中,也會丟擲錯誤,至此,redux庫中最核心的派發功能已經實現。
插一句,在redux庫中預設呼叫了一次dispatch方法,為什麼要先呼叫一次呢?因為預設狀態下,內部的currentState物件為undefined
,為了保證狀態已賦初始值,我們要手動呼叫一下dispatch方法(因為初始化狀態是由外界提供),並傳入一個初始化動作:
//執行一次派發,以保證state初始化
dispatch({
type: `@@redux/INIT`
});
複製程式碼
@@redux/INIT
這個動作本無實際意義,其目的就是為了初始化狀態物件,為什麼叫這個名字呢?我理解只是想起個逼格高點的名字。
訂閱與退訂
當狀態樹更新,隨之可能要做一些後續操作,比如Web開發中要更新對應的檢視,而讓開發者自己呼叫顯然不是一個友好的做法,因此我們可以參照“釋出-訂閱”模式來實現訂閱功能。
方法很簡單,使用一個陣列記錄下訂閱的函式,當派發動作完成,即按順序執行“訂閱”即可:
function createStore(reducer) {
var listeners = []; //儲存訂閱回撥
/**
* 訂閱
* @param {Function} listener 監聽函式
* @returns {Function} 返回退訂函式
*/
function subscribe(listener) {
listeners.push(listener);
return function () {
listeners = listeners.filter(fn => fn != listener);
}
}
//其它程式碼略...
}
複製程式碼
subscribe方法是一個高階函式,傳入了外界的訂閱回撥,並追加到listener陣列中,返回的仍是一個函式,即退訂。
這樣再次執行退訂函式即過濾掉了當前回撥,完成了退訂操作,這就是使用“釋出-訂閱”模式的實現。
最後,別忘了在dispatch方法中呼叫訂閱函式:
listeners.forEach(fn => fn());
複製程式碼
生成actionCreator
回顧一下在使用redux開發的過程中,我們一般都使用一個函式來返回action物件,這樣做的好處是避免手寫長長的ActionType,免得出錯:
//ActionCreator例子:
function displayBook(payload){
return {type:`DISPLAY_BOOK`, payload};
}
複製程式碼
這樣通過呼叫函式的方式displayBook(1001)
就返回了相應的action物件。接下來派發即可:store.dispatch(displayBook(1001))
而得到了action之後的工作就是派發,每次如果都手動呼叫store.dispatch()
顯得很冗餘,因此redux提供了bindActionCreator方法,它的功能就是將dispatch功能封裝到actionCreator函式裡,可以讓開發者節省一步呼叫dispatch的操作,我們實現它。
新建一個bindActionCreators.js檔案,我們寫出函式簽名:
/**
* 建立ActionCreators
* 將派發動作封裝到原actionCreator物件裡面
* @param {Object} actionCreators 物件集合
* @param {Function} dispatch redux派發方法
*/
function bindActionCreators(actionCreators, dispatch) {
}
複製程式碼
可以看到傳入的是一個由每個actionCratore封裝好的物件,其原理非常簡單,迴圈物件中每一個actionCreator方法,將dispatch方法的呼叫重寫到新函式裡即可:
function bindActionCreators(actionCreators, dispatch) {
var boundActions = {};
Object.keys(actionCreators).forEach(key => {
//將每個actionCreator重寫
boundActions[key] = function (...args) {
//將派發方法封裝到新函式裡
dispatch(actionCreators[key](...args));
};
});
return boundActions;
}
複製程式碼
經過bindActionCreator的處理之後,可以將程式碼進一步精簡:
var actionCreator = bindActionCreators({displayBook},store.dispatch);
複製程式碼
直接呼叫
actionCreator.displayBook(1001)
即派發了DISPLAY_BOOK動作。
合併reducer
隨著redux專案的越來越複雜,reducer的業務邏輯也越來越多,如果將所有的業務都放在一個reducer函式中顯然很拙劣,通常我們使用react結合redux開發時,reducer與元件相對應,因此按元件功能來拆分reducer會更好的管理程式碼。
redux提供了combineReducers來實現將多個reducer合併為一個,我們先來回顧一下它的用法:
import { combineReducers } from `redux`;
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
//chatReducer函式即合併後的reducer
複製程式碼
可以看到它的用法和之前的bindActionCreators類似,仍是將每個reducer封裝為一個物件傳入,返回的結果即合併後的reducer。
使用時需注意的是,combineReducers以reducer的名稱來合併為一個最終的大state物件:
建立一個combineReducers.js,來實現合併reducer方法:
/**
* 合併reducer
* @param {Object} reducers reducer集合
* @returns {Function} 整合後的reducer
*/
function combineReducers(reducers) {
return function (state = {}, action) {
let combinedState = {}; //合成後的state物件
Object.keys(reducers).forEach(name => {
//執行每一個reducer,將返回的state掛到 combinedState中,並以reducer的名字命名
combinedState[name] = reducers[name](state[name], action);
});
return combinedState;
}
}
複製程式碼
可見,原理和同樣是迴圈物件中的每一個reducer,使用reducer名稱來合併為最終的reducer函式。
這樣高階函式返回的方法一定要按照reducer的名稱來分類即可。至此redux庫的核心程式碼已經實現完畢。
下一篇文章手寫一下另一塊內容:redux中介軟體原始碼