面試還問redux?那我從頭手擼原始碼吧(核心程式碼)

恍然小悟發表於2019-03-04

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

redux原始碼我大致分了3塊,從易到難:

  • 狀態管理核心程式碼
  • react-redux庫
  • 中介軟體

手寫原始碼不是目的,主要是為了看看大牛寫的程式碼更能開拓思維,以後和麵試官扯淡的時候能把他忽悠住。 下面從零開始,手擼一套自己的redux庫,預期與官方庫達到近似的功能,並且比較官方原始碼,看看自己的寫法有哪些不足。今天先從redux核心程式碼開始。

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物件:

面試還問redux?那我從頭手擼原始碼吧(核心程式碼)

建立一個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中介軟體原始碼

相關文章