面試還問redux?那我從頭手擼原始碼吧(一)
最近處在專案的間歇期,沒事參加了幾場面試發現面試官依然喜歡問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.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庫的核心程式碼已經實現完畢。等抽出時間再總結一下react-redux庫及中介軟體的原始碼。