面試還問redux?那我從頭手擼源碼吧(核心代碼)

最近處在項目的間歇期,沒事參加了幾場面試發現面試官依然喜歡問redux的一些問題,尤爲是問這種開發框架的問題最好的辦法就是撤底搞懂其源碼,正好利用這兩天時間從頭過了一遍redux庫,仍是有些收穫的。javascript

redux源碼我大體分了3塊,從易到難:java

  • 狀態管理核心代碼
  • react-redux庫
  • 中間件

手寫源碼不是目的,主要是爲了看看大牛寫的代碼更能開拓思惟,之後和麪試官扯淡的時候能把他忽悠住。 下面從零開始,手擼一套本身的redux庫,預期與官方庫達到近似的功能,而且比較官方源碼,看看本身的寫法有哪些不足。今天先從redux核心代碼開始。react

redux核心代碼實現

動手以前先回顧一下redux是幹什麼的,它能解決什麼問題?redux的出現就是爲了解決react組件的狀態管理。redux內部管理了一個狀態樹(state),根據開發者提供的reducer來「派發」一個「動做」以更新state,這樣數據管理所有交由redux來處理而不在由react組件去操心。其實redux只是一種數據管理的設計思想,而不是一個用於react中的特定框架,所以只要咱們的業務足夠複雜,脫離react在任何環境下都能使用redux。面試

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中間件源碼

相關文章
相關標籤/搜索