前情提要
认识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
是对指定 type
的 action
进行响应,从而对初始的 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
值的reducer
和action
的描述语以及提示。
- 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
,因为当使用store
的replaceReducer
时会自动触发该内置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)环境下对类型为undefined
的reducer
进行了过滤和打印警告处理,其后又将符合规范的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
需要注意传入的对象每个键必须对应一个类型为function
的reducer
(废话
请大家记住这几个点,在这些前提下能够帮助你更快的理解我们的combineReducers
感谢你的阅读~