前情提要

  • 逐行阅读redux源码(一)createStore

认识reducers

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

如图所见,我们可以明白, 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.typeconst 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) returnif (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 unexpectedKeyCacheif (process.env.NODE_ENV !== 'production') {unexpectedKeyCache = {}}let shapeAssertionErrortry {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 = falseconst 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] = nextStateForKeyhasChanged = 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

感谢你的阅读~

逐行阅读redux源码(二)combineReducers相关推荐

  1. 阅读react-redux源码(二) - createConnect、match函数的实现

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 上一节看了Provider组 ...

  2. 阅读react-redux源码(七) - 实现一个react-redux

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  3. 阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  4. 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  5. 阅读react-redux源码(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  6. 阅读react-redux源码 - 一

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  7. 阅读react-redux源码 - 零

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 react的技术栈一定会遇到 ...

  8. 阅读react-redux源码(六) - selectorFactory处理store更新

    阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...

  9. redux源码分析之二:combineReducers.js

    欢迎关注redux源码分析系列文章: redux源码分析之一:createStore.js redux源码分析之二:combineReducers.js redux源码分析之三:bindActionC ...

最新文章

  1. scapy on openwrt
  2. 《大话数据结构》第9章 排序 9.5 直接插入排序
  3. 判断点是否在凸多边形内
  4. 【图解计算机组成原理】第1章 计算机系统概论
  5. 设置元素的宽和高 元素的left和top 元素卷曲出去的值 为元素绑定事件
  6. 【slighttpd】基于lighttpd架构的Server项目实战(4)—简单的echo服务器
  7. SAP UI5 应用开发教程之十九 - SAP UI5 数据类型和复杂的数据绑定
  8. C语言丨格式化屏幕输出(二)——日历
  9. 外卖返利淘宝客小程序公众号淘宝客APP花卷云美团饿了么返利系统
  10. 考研高等数学张宇30讲笔记——第八讲 一元函数积分学的概念与计算
  11. html鼠标自动向下滑动,win10电脑中鼠标自动向下或向上滚动怎么解决
  12. android studio ADT+SDK \appium下载与安装
  13. a5松下驱动器参数设置表_松下a5伺服参数设置详解
  14. postgresql安装postgis扩展模块
  15. netty报错:远程主机强迫关闭了一个现有的连接
  16. 怎么判断两个多项式互素_多项式互素的等价条件
  17. 网易云发布“工业智能平台”,开放技术赋能工业企业
  18. 个人博客园样式、背景及细节美化过程
  19. C#测试调用PaddleOCRSharp模块识别图片文字
  20. python 如何使用 pandas 在 flask web 网页中分页显示 csv 文件数据

热门文章

  1. html在线测试接口,apiManager: 小幺鸡在线接口管理系统,支持在线测试,支持json,txt,xml,html,js,流,和 WebSocket...
  2. 同步服务老是报错_SQL2005的维护计划做异地备份,采用另一台服务器设置共享文件夹的方式同步。但是一直报错,本地备份可以...
  3. rs232串口驱动_电机驱动器-copley
  4. java访问手机里的通讯录,访问手机通讯录(示例代码)
  5. .java编写一个梯形类lader_能够完成相关计算above为高_【Java】编写一个应用程序计算梯形和圆形的面积...
  6. mysql proxy性能差_两种MySQL-Proxy架构的测试对比记录
  7. 办公室自动化系统_大队举办办公自动化系统培训班
  8. Miniconda3+PyTorch1.7.1(GPU版)+Win10_x64+GTX1060深度学习环境搭建
  9. mysql autocommit问题导致的gtid同步变慢
  10. [java手把手教程][第二季]java后端博客系统文章系统——No10