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