逐行閱讀redux原始碼(二)combineReducers

santree發表於2018-11-14

前情提要

認識reducers

在我們開始學習原始碼之前,我們不妨先來看看何謂reducers:

image

如圖所見,我們可以明白, reducer 是用來對初始的狀態樹進行一些處理從而獲得一個新的狀態樹的,我們可以繼續從其使用方法看看 reducer 到底如何做到這一點:

function reducerDemo(state = {}, action) {
  switch (action.type) {
    case 'isTest':
      return {
        isTest: true
      };
    default:
      return state;
  }
}
複製程式碼

從我們的 reducerDemo 中,我們可以看到 reducer 接受了兩個引數:

  • state
  • action

通過對 action 中的 type 的判斷,我們可以用來確定當前 reducer 是對指定 typeaction 進行響應,從而對初始的 state 進行一些修改,獲得修改之後的 state 的。從之前我們在 createStore 中看到的情況:

currentState = currentReducer(currentState, action)
複製程式碼

每次 reducer 都會使用上一次的 state,然後處理之後獲得新的 state

但是光是如此的話,在處理大型專案的時候我們似乎有點捉襟見肘,因為一個store只能接受一個reducer,在大型專案中我們通常會有非常非常多的 action 用來對狀態樹進行修改,當然你也可以在 reducer 中宣告海量的 switch...case.. 來實現對單個action的響應修改,但是當你這樣做的時候,你會發現你的reducer越來越大,處理過程越來越複雜,各個業務邏輯之間的耦合度越來越高,最後你就會發現這個 reducer 將完全無法維護。

所以為了解決在大型專案中的這類問題,我們會使用多個reducer,每個reducer會去維護自己所屬的單獨業務,但是正如我們之前所說,每個store只會接受一個 reducer,那我們是如何將reducer1、reducer2、reducer3、reducer4整合成一個reducer並且返回我們所需的狀態樹的呢?

combineReducers

當然我們能想到的問題,redux 肯定也能想到,所以他們提供了 combineReducers api讓我們可以將多個 reducer 合併成一個 reducer ,並根據對應的一些規則生成完整的狀態樹,so,讓我們進入正題,開始閱讀我們 combineReducers 的原始碼吧:

依賴

首先是combineReducers的依賴,我們能在程式碼的頭部找到它:

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
複製程式碼

可以看到,combineReducers僅僅依賴了之前我們在上一篇文章中提到的工具類:

  • ActionTypes(內建的actionType)
  • warning(顯式列印錯誤)
  • isPlainObject(檢測是否為物件)

錯誤資訊處理

進入正文,在combineReducers的開始部分,我們能夠發現許多用於返回錯誤資訊的方法:

  • getUndefinedStateErrorMessage(當reducer返回一個undefined值時返回的錯誤資訊)
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action'

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}
複製程式碼

從方法可知,這個處理過程中,我們傳入了key(reducer的方法名)以及action物件,之後根據action中是否存在type獲得了action的描述,最終返回了一段關於出現返回undefined值的reduceraction的描述語以及提示。

  • getUnexpectedStateShapeWarningMessage(獲取當前state中存在的沒有reducer處理的狀態的提示資訊)
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}
複製程式碼

在說這段原始碼之前,我們需要稍微瞭解一下,當我們使用combineReucers,我們傳入的reducer的資料結構:

function reducer1(state={}, action) {
    switch (action.type) {
    case 'xxx':
      return true;
    default:
      return state;
  }
}

function reducer2() {...}
function reducer3() {...}
function reducer4() {...}

const rootReducer = combineReucers({
    reducer1,
    reducer2,
    reducer3,
    reducer4
})
複製程式碼

我們傳入的時以reducer的方法名作為鍵,以其函式作為值的物件,而使用rootReducer生成的store會是同樣以每個reducer的方法名作為鍵,其reducer處理之後返回的state作為值的物件,比如:

// 生成的state
{
    reducer1: state1,
    reducer2: state2,
    reducer3: state3,
    reducer4: state4
}
複製程式碼

至於為何會這樣,我們後面再提,現在先讓我們繼續往下閱讀這個生成錯誤資訊的方法。

在這個方法中,其工作流程大概如下:

  • 宣告reducerKeys獲取當前合併的reducer的所有鍵值
  • 宣告argumentName獲取當前是否為第一次初始化store的描述
  • 當不存在reducer的時候返回拋錯資訊
  • 當傳入的state不是一個物件時,返回報錯資訊。
  • 獲取state上未被reducer處理的狀態的鍵值unexpectedKeys,並將其存入cache值中。
  • 檢測是否為內建的replace action,因為當使用storereplaceReducer時會自動觸發該內建action,並將reducer替換成傳入的,此時檢測的reducer和原狀態樹必然會存在衝突,所以在這種情況下檢測到的unexpectedKeys並不具備參考價值,將不會針對性的返回拋錯資訊,反之則會返回。

通過如上流程,我們將能對未被reducer處理的狀態進行提示。

  • assertReducerShape(檢測reducer是否符合使用規則)
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === 'undefined'
    ) {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

複製程式碼

相對之前的多次判斷,這個就要簡單暴力的多了,直接遍歷所有的reducer,首先通過傳入undefined的初始值和內建的init action,如果不能返回正確的值(返回了undefined值),那麼說明reducer並沒有針對預設屬性返回正確的值,我們將提供指定的報錯資訊。

這之後又使用reducer處理了undefined初始值和內建隨機action的情況,這一步的目的是為了排除使用者為了避免第一步的判斷,從而手動針對內建init action進行處理,如果使用者確實做了這種處理,就丟擲對應錯誤資訊。

如此,我們對combineReucers的錯誤資訊處理已經有了大概的瞭解,其大致功能如下:

  • 判斷reducer是否是合規的
  • 找出哪些reducer不合規
  • 判斷狀態樹上有哪些沒有被reducer處理的狀態

瞭解了這些之後,我們便可以進入真正的combineReducers了。

合併reducers

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
複製程式碼

首先我們看到變數宣告部分:

  • reducerKeys (reducer在物件中的方法名)
  • finalReducers (最終合併生成的的reducers)

接下來,該方法迴圈遍歷了reducerKeys,並在產品級(production)環境下對型別為undefinedreducer進行了過濾和列印警告處理,其後又將符合規範的reducer放到了finalReducer中,這一步是為了儘量減少後面的流程受到空值reducer的影響。

然後combineReducers進一步的對這些非空reducer進行了處理,檢測其中是否還有不合規範的reducer(通過assertReducerShape),並通過try catch 將這個錯誤儲存到shapeAssertionError變數中。

正如我們一直所說,reducer需要是一個function,所以我們的combineReducer將是一個高階函式,其會返回一個新的reducer,也就是原始碼中的combination

在返回的combination中,會檢測是否有shapeAssertionError,如果有呼叫該reducer時將終止當前流程,丟擲一個錯誤,並且在產品級環境下,還會檢測是否有未被reducer處理的state並列印出來進行提示(不中斷流程)。

最後才是整個combination的核心部分,首先其宣告瞭一個變數來標識當前狀態樹是否更改,並宣告瞭一個空的物件用來存放接下來會發生改變的狀態,然後其遍歷了整個finalReducer,通過每個reducer處理當前state,並將其獲得的每個值和之前狀態樹中的對應key值得狀態值進行對比,如果不一致,那麼就更新hasChanged狀態,並將新的狀態值放到指定key值得state中,且更新整個狀態樹,當然其中還是會對出現異常state返回值的異常處理。

結語

到此,我們已經通讀了combineReducers中的所有程式碼,也讓我們稍微對使用combineReducer時需要注意的幾個點做一個總結:

  • 每個reducer必須要有非undefined的返回值
  • 不要使用reducer手動去操作內建的action
  • combineReducers需要注意傳入的物件每個鍵必須對應一個型別為functionreducer(廢話

請大家記住這幾個點,在這些前提下能夠幫助你更快的理解我們的combineReducers

感謝你的閱讀~

相關文章