逐行阅读redux源码(二)combineReducers

栏目: IOS · Android · 发布时间: 6年前

内容简介:在我们开始学习源码之前,我们不妨先来看看何谓reducers:如图所见,我们可以明白,从我们的

在我们开始学习源码之前,我们不妨先来看看何谓reducers:

逐行阅读redux源码(二)combineReducers

如图所见,我们可以明白, 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

感谢你的阅读~


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

欲罢不能

欲罢不能

[美] 亚当·奥尔特 / 闾佳 / 机械工业出版社 / 2018-4-1 / 59.00元

全面揭秘和解决“行为上瘾”的奠基之作 美国亚马逊分类图书畅销榜第一名 行为上瘾是什么?诱人上瘾的体验是如何设计出来的? 如何远离行为上瘾?如何用行为上瘾做些好事? ◆ 内容简介 ◆ 欢迎来到“行为上瘾”的时代! 我们中近半数人至少有一种“行为上瘾”:无时无刻盯着手机,不断刷朋友圈,通宵追看电视剧集,没日没夜打游戏,频繁查看邮件,用太多时间工作…… 而那些生......一起来看看 《欲罢不能》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具