最近處在項目的間歇期,沒事參加了幾場面試發現面試官依然喜歡問redux的一些問題,尤爲是問這種開發框架的問題最好的辦法就是撤底搞懂其源碼,正好利用這兩天時間從頭過了一遍redux庫,仍是有些收穫的。javascript
redux源碼我大體分了3塊,從易到難:java
手寫源碼不是目的,主要是爲了看看大牛寫的代碼更能開拓思惟,之後和麪試官扯淡的時候能把他忽悠住。 下面從零開始,手擼一套本身的redux庫,預期與官方庫達到近似的功能,而且比較官方源碼,看看本身的寫法有哪些不足。今天先從redux核心代碼開始。react
動手以前先回顧一下redux是幹什麼的,它能解決什麼問題?redux的出現就是爲了解決react組件的狀態管理。redux內部管理了一個狀態樹(state),根據開發者提供的reducer來「派發」一個「動做」以更新state,這樣數據管理所有交由redux來處理而不在由react組件去操心。其實redux只是一種數據管理的設計思想,而不是一個用於react中的特定框架,所以只要咱們的業務足夠複雜,脫離react在任何環境下都能使用redux。面試
redux核心具備如下功能:redux
咱們一一實現這些功能。數組
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());
複製代碼
回顧一下在使用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動做。
隨着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中間件源碼