逐行閱讀redux源碼(二)combineReducers

前情提要

認識reducers

在咱們開始學習源碼以前,咱們不妨先來看看何謂reducers:redux

image

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

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

從咱們的 reducerDemo 中,咱們能夠看到 reducer 接受了兩個參數:bash

  • state
  • action

經過對 action 中的 type 的判斷,咱們能夠用來肯定當前 reducer 是對指定 typeaction 進行響應,從而對初始的 state 進行一些修改,得到修改以後的 state 的。從以前咱們在 createStore 中看到的狀況:數據結構

currentState = currentReducer(currentState, action)
複製代碼

每次 reducer 都會使用上一次的 state,而後處理以後得到新的 stateless

可是光是如此的話,在處理大型項目的時候咱們彷佛有點捉襟見肘,由於一個store只能接受一個reducer,在大型項目中咱們一般會有很是很是多的 action 用來對狀態樹進行修改,固然你也能夠在 reducer 中聲明海量的 switch...case.. 來實現對單個action的響應修改,可是當你這樣作的時候,你會發現你的reducer愈來愈大,處理過程愈來愈複雜,各個業務邏輯之間的耦合度愈來愈高,最後你就會發現這個 reducer 將徹底沒法維護。dom

因此爲了解決在大型項目中的這類問題,咱們會使用多個reducer,每一個reducer會去維護本身所屬的單獨業務,可是正如咱們以前所說,每一個store只會接受一個 reducer,那咱們是如何將reducer一、reducer二、reducer三、reducer4整合成一個reducer而且返回咱們所需的狀態樹的呢?ide

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僅僅依賴了以前咱們在上一篇文章中提到的工具類:post

  • 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

感謝你的閱讀~

相關文章
相關標籤/搜索