Redux原始碼分析(2) - createStore

夕陽楓發表於2020-04-21

1、前言

  下邊章節中將詳細分析原始碼,原始碼分析中對於一些邊界的判斷、型別判斷等不做重點分析,主要將分析的重點放在主流程方向上。

2、createStore

   createStore 作為 Redux 的核心 api 之一,其作用是通過 reducer 和 中介軟體 middleware 構造一個為 store 的資料結構。關於 createStore 的使用可參考上一章節 Redux原始碼分析(1) - Redux介紹及使用 和 官方文件 createStore

  createStore 的原始碼結構圖如下圖所示。根據是否傳入enhancer,分別做不同的邏輯判斷
createStore原始碼結構圖

2.1 存在enhancer

   當存在enhancer的時候,實際執行語句如下:

//createStore.js

return enhancer(createStore)(reducer, preloadedState);  //記為語句(1)

   在上一章節中,我們介紹過 Redux 的如何使用:

//demo

let store = createStore(rootReducer, applyMiddleware(Logger, Test));   //記為語句(2)

   由語句(2)可知:enhancer 其實就是 applyMiddleware 作用中介軟體(此處為Logger, Test)的結果。檢視 applyMiddleware 原始碼,大致結構如下:

//applyMiddleware.js

function applyMiddleware(...middlewares){
    //createStore中對於的enhancer
    return createStore => (...args) => {
    //...
      return {
        ...store,
        dispatch  // 覆蓋store中的dispatch
      };
    }
}

   由 applyMiddleware 的原始碼可知。語句(1)執行的就是 applyMiddleware 返回高階函式的完整執行,最終返回的結果是包含 store 所有屬性 和 dispatch屬性的一個物件,這也與沒有enhancer時,createStore輸出的結果保持之一。因為 store 物件中本身就存在 dispatch 屬性,由此可知 :

   applyMiddleware 作用中介軟體的結果就是更改 store 物件的dispatch。這是前文提到過的,applyMiddleware 本質是對 dispatch 的增強。至於是如何增強的,在下文會詳細分析。

2.2 不存在enhancer

   首先看下此時對應的返回結果

//createStore.js

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
};
2.2.1 方法內的區域性變數
let currentReducer = reducer;  // 臨時reducer
let currentState = preloadedState; //當前的state值,預設為初始化preloadedState
let currentListeners = [];  // 監聽佇列,用於存放監聽事件, 釋出-訂閱模式
let nextListeners = currentListeners; // 淺拷貝下這個佇列
let isDispatching = false; // 標誌位,用來判斷是否有存在正在執行的dispatch

   各個變數的作用,註釋中已經詳細註明,不再贅述。

2.2.2 getState

   根據 Redux api 可知:getState 方法返回當前的 state 樹。currentState 預設值 為 preloadedState,具體的
currentState 取決於 dispatch(action) 時 reducer 執行後返回的結果。其中如果 isDispatching 為 true 時,表示有 dispatch 正在執行,此時獲取 state 的值會導致獲取不到正確的 state。


function getState() {
    if (isDispatching) {
        .....
    }
    
    return currentState;
}
2.2.3 subscribe

   先來看看subscriber的使用,其中 listener 表示監聽觸發時,需要做的一些操作。


let unsubscribe = store.subscribe(listener)
unsubscribe()

   subscribe 的原始碼如下:


function subscribe(listener) {
    // 型別判斷
    if (typeof listener !== 'function') {
        throw new Error('Expected the listener to be a function.');
    }

    // 同理不可以dispatch中
    if (isDispatching) {
        //……
    }

    // 用來表示訂閱標記,用於避免取消訂閱後再次取消
    let isSubscribed = true;
    // ensureCanMutateNextListeners幹啥的,點選去看一下
    ensureCanMutateNextListeners();
    // 將 listener 存在在 釋出-訂閱模式的監聽佇列 nextListeners 中
    nextListeners.push(listener);
    // 返回取消的function(unsubscribe)
    return function unsubscribe() {
        // 如果已經取消訂閱 直接直接return
        if (!isSubscribed) {
            return;
        }

        // 同理
        if (isDispatching) {
            //……
        }

        // 這裡標記為 已經取消訂閱
        isSubscribed = false;
        // 儲存訂閱快照
        ensureCanMutateNextListeners();
        // 根據索引 在監聽佇列裡刪除監聽
        const index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1);
    };
}

   通過原始碼的解讀可知。listener 必須傳入一個 function 型別,否則就會報錯。這裡用到了釋出-訂閱模式,執行 subscribe 方法時,將傳入的 listener 存放在 監聽佇列 nextListeners 裡,currentListeners 和 nextListeners 都是引用型別,都是指向的一個記憶體地址,可以理解為是一個東西。

   返回值是一個 unsubscribe 函式。執行該函式,就能夠取消訂閱。具體來看首先判斷 isSubscribed 是否為 false,如果是則代表已經取消了該訂閱,再次執行改訂閱則直接忽視。如果 isSubscribed 為 ture 則表示該訂閱還存在,則通過 indexOf 方法找到索引後,通過 splice 方法,將該訂閱從訂閱佇列中取消,同時不要忘記將 isSubscribed 設定為已經 false (表示已經取消)。

2.2.4 dispatch

   dispatch的使用方式如下。分發 action是觸發 state 變化的惟一途徑。匹配到對應的 reducer 執行之後,會返回一個新的 state。

store.dispatch(action)

   dispatch 的原始碼如下:

function dispatch(action) {
        // acticon必須是由Object構造的函式, 否則throw Error
        if (!isPlainObject(action)) {
            // 丟擲錯誤
        }

        // 判斷action, 不存在type throw Error
        if (typeof action.type === 'undefined') {
           // 丟擲錯誤 'Have you misspelled a constant?'
            );
        }

        // dispatch中不可以有進行的dispatch
        if (isDispatching) {
           // 丟擲錯誤
        }

        try {
            // 執行時標記為true
            isDispatching = true;
            // reducer形式如下:(state,action)=>{} , reducer本身就是個函式,由此可見dispatch(action), 就是執行reducer方法,並將currentState,action作為引數
            currentState = currentReducer(currentState, action);
        } finally {
            // 最終執行, isDispatching標記為false, 即完成狀態·
            isDispatching = false;
        }

        // 迴圈監聽佇列,並執行每一個監聽事件
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            // 執行每一個監聽函式
            listener();
        }
        // 返回傳入的action
        return action;
    }

   通過原始碼的解讀可知。action 必須是一個純粹的物件且必須包含type屬性,否則就出丟擲異常。dispatch 方法幹了兩件事情:

  • 1、找到對應的 reducer 根據 action.type 執行對應的分支,並返回最新的 state 。 為什麼說對應的 reducer,因為 reducer 有可能通過 combineReducers 組合的,此時 action.type 只會更改對應的 reducer 的返回值。
currentReducer(currentState, action);

如果 reducer 不是組合生成,以 reducer/todos 為例。則  currentReducer 就是如下:

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO': //……

    case 'TOGGLE_TODO': //……
     
    default:
      return state
  }
}

如果 reducer 組合生成,也即本例中 rootReducer (通過 combineReducers 組合)。檢視 combineReducers 原始碼可知,  combineReducers 組合各個 reducer 後返回的是一個名為 combination 的高階函式,  也就是currentReducer ,有如下形式:

function combination(state = {}, action) {
    // ……
    return state
}

二者具有相同的形式

(state , action ) =>{ // 更改 state的邏輯}

   通過上邊的虛擬碼分析可以,不管是否通過 combineReducers 組合生成, currentReducer 都具有相同的形式, 本身作為一個函式接受引數 state 和 action ,並根據action,改變state的值。因此可以認為,dispatch(action) 時,本質就是 reducer 方法的執行,並將當前的 stare 值 currentState 和 action 作為引數,並返回一個新的 state。

  • 2、執行通過 subscribe 方法新增的監聽, 通過 subscribe 方法新增的監聽都被記錄在監聽佇列 currentListeners 中,在dispatch 方法會迴圈遍歷監聽佇列,並以此執行各個佇列元素。
2.2.5 replaceReducer

   在 createStore 方法中,還要其他一些我們不常用的 api

  • replaceReducer : 更改 reducer

這是一個高階 API。只有在你需要實現程式碼分隔,而且需要立即載入一些 reducer 的時候才可能會用到它。在實現 Redux 熱載入機制的時候也可能會用到observable:

3、reducer 的初始化。

dispatch({ type: ActionTypes.INIT });


const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  //……
}

   由上邊的程式碼可知,初始化 reducer 時是通過 dispatch 一個隨機產生的 action 。根據 reducer 的特性可知,當前初始化的 dispatch 會執行對應的 default 分支,也即會輸出 reducer 中預設的 state 的值。

   與 createStore 引數 preloadedState 的對比:在 createStore 方法的定義中可以接受一個preloadedState 引數,該引數會預設為當前的 state。

let currentState = preloadedState; //當前的state值,預設為初始化preloadedState

   通過 dispatch 的流程可知,初始化dispatch時,preloadedState 會作為 reducer 方法執行的引數傳入。當 preloadedState 不存在時,此時 reducer 的入參為 undefined。通常做法如下, 通過 es6 的預設引數給state 複製初始值,則能起到 preloadedState 的效果。猜測 Redux 這些寫應該是為了相容 es5 的預設值處理吧。

const todos = (state = [], action) => {//.......}

   但是不管 preloadedState 指定與否,初始化dispatch 執行後,currentState 的值即為default 分支對應的值。由此可知,我們定義的 reducer 都要包含 default 分支,否則初始化後 state 的值就會出現異常。


Redux原始碼分析(1) - Redux介紹及使用
redux原始碼分析(2) - createStore
Redux原始碼分析(3) - applyMiddleware
Redux原始碼分析(4) - combineReducers和 bindActionCreators

相關文章