前言:上一篇文章讲完了redux的基础核心部分,我们知道在实际开发中,我们会将reducer进行拆分,这样便于代码的阅读与维护,数据管理更加清晰。而通过上一篇文章,我们知道createStore只能接受一个reducer作为参数,如果我们将reducer进行拆分,那么怎么将多个reducer传入createStore函数呢?接下来,我们就来聊聊combineReducers的作用。
首先我们先来看看combineReducers的使用
//管理用户信息相关的reducer
function userReducer(state={},action) {
return state
}
//管理菜单信息相关的reducer
function menusReducer(state=[],action) {
return state
}
//reducer合并
const reducers = combineReducers({userReducer,menusReducer})
//这是ES6对象的简写,相当于
//{userReducer:userReducer,menusReducer:menusReducer}
//创建store
const store = createStore(reducers)
现在我们来看看combineReducers内部做了什么?combineReducers.js的源码如下
import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
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.`
)
}
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.`
)
}
}
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.`
)
}
})
}
/**
* Turns an object whose values are different reducer functions, into a single
* reducer function. It will call every child reducer, and gather their results
* into a single state object, whose keys correspond to the keys of the passed
* reducer functions.
*
* @param {Object} reducers An object whose values correspond to different
* reducer functions that need to be combined into one. One handy way to obtain
* it is to use ES6 `import * as reducers` syntax. The reducers may never return
* undefined for any action. Instead, they should return their initial state
* if the state passed to them was undefined, and the current state for any
* unrecognized action.
*
* @returns {Function} A reducer function that invokes every reducer inside the
* passed object, and builds a state object with the same shape.
*/
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
}
}
我们直接来看combinReducers函数,它只接受一个参数reducers,这个reducers是什么呢?按照我们上面的使用例子,这个reducers的值就是
{
userReducer:function(){...},
menusReducers:function(){...}
}
接下来我们看看函数内部的实现
//获取传入对象的键名,返回键名的数组
const reducerKeys = Object.keys(reducers)
//声明一个空对象,用来保存最终的reducers
const finalReducers = {}
此时,reducerKeys的值为 ["userReducer","menusReducer"]数组
//遍历reducerKeys
for (let i = 0; i < reducerKeys.length; i++) {
//获取每一个键名
const key = reducerKeys[i]
//如果是在开发环境,则进行判断,如果reducer是undefined,则提示错误信息
//例如,我们在调用combineReducers是传入{user:undefined}这样一个对象。
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
//判断传入的对象对应键的值是不是一个函数类型
if (typeof reducers[key] === 'function') {
//如果是的话,则存入finalReducers里
finalReducers[key] = reducers[key]
}
}
经过上面的遍历后,将传入的reducers拷贝给了finalReducers,这里相当于进行了一次深拷贝。
//获取finalReducers对象的键名数组
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
//声明一个变量
let shapeAssertionError
try {
//调用assertReducerShape函数,传入finalReducers对象
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
上面的代码,首先获取finalReducers对象的键名,保存为一个数组;声明一个变量unexpectedKeyCache,如果是开发环境,将其值赋值为一个空对象。接下来声明了一个变量shapeAssertionError,用来保存错误信息。然后调用了一个函数,finalReducers作为参数传入,并且这个函数的调用使用try...catch包裹起来,当有错误的时候讲错误信息赋值给shapeAssertionError,所以我们知道这个函数的调用可能会抛出错误信息,由此猜测函数的作用是用来对finalReducers对象的数据结构进行检测的,也就是我们开始调用combineReducers传入的对象。
assertReducerShape函数的源码如下:
function assertReducerShape(reducers) {
//获取reducers的键,得到一个数组,然后遍历此数组
Object.keys(reducers).forEach(key => {
//获取每个键对应的reducer
const reducer = reducers[key]
//调用reducer,获取初始化state,这里我们第一个参数传入undefined,第二个传入的是一个action,
//type类型是一个随机字符串,上一篇已经讲过,这样做的目的是为了让reducer返回当前state,就是
//当前传入的state,在这里也就是undefined
const initialState = reducer(undefined, { type: ActionTypes.INIT })
//判断initialState的值是不是undefined,如果是的话则报错
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.`
)
}
/**
* 在这里,可能很多人会有疑问,传入的state为undefined,执行的时候又走默认返回值,即直接返回state,
* 这个initialState不肯定是undefined吗?还以上面的使用为例,我们的userReducer如下
* function userReducer(state={},action) {return state}
* 我们在声明的时候给state给了默认值,所以当我们传入undefined时,实际上得到的是{}空对象,所以这里
* 的作用就是为了让我们声明reducer的时候,state必须给一个默认值。或者在调用createStore的时候传入
* preloadedState参数,也就是state的初始值
*/
/**
* 这里跟上面差不多,传入undefined和一个随机的action.type,判断reducer的返回值是否为undefined,
* 对于你disptach的任何一个action,如果type是没有在reducer里命名的,应该返回当前state,并且当前的
* state不能是undefined值,但是可以是null
*/
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.`
)
}
})
}
接下来我们看最后一段代码,我们知道createStore接受的第一个参数reducer必须是一个函数,所以combineReducer的返回值肯定是一个函数。
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
}
第一个判断上面已经讲过,接下我们看看第二个if判断的作用
function getUnexpectedStateShapeWarningMessage(
inputState,
reducers,
action,
unexpectedKeyCache
) {
//获取reducers对象的key的数组
const reducerKeys = Object.keys(reducers)
//判断action的类型,如果是ActionTypes.INIT,说明是初始化,则此时state是调用createStore时
//传入的preloadedState初始化值。否则的话就是通过reducer传入的上一次的state值
const argumentName =
action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer'
//如果reducerKeys数组的长度为0,则返回一段描述错误的字符串,即我们传入combineReducers的对象是一个空对象{}
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.'
)
}
//判断state类型,必须是一个纯对象
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('", "')}"`
)
}
//遍历state,得到不是期望的键的数组
const unexpectedKeys = Object.keys(inputState).filter(
//当这个键不是state自身的,是它原型链上的,并且unexpectedKeyCache中不存(unexpectedKeyCache值为{})
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
//将state中不是预期的key加入到unexpectedKeyCache中,值设置为true
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})
//如果action.type属于替换类型,也就是替换reducer的时候,直接返回,不做下面的检查
if (action && action.type === ActionTypes.REPLACE) return
//判断unexpectedKeys的长度是否大于0,如果大于0则返回一个错误的提示信息字符串,
//这里的作用是,如果你在初始化state上的某个属性不是本身的属性,而是它原型链上的,
//如果你改变state时,这个属性将被忽略,什么意思呢?举个例子,假如你的初始化state = {age:23},
//而state的原型对象上有一个属性name:'张三',这时,在应用中,这两个属性你都能访问到,但是,如果你
//dispatch一次,这个name属性就会丢失了,因为redux更新state是整个替换。
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.`
)
}
}
下面是combineReducers最主要的代码了
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
首先,声明一个布尔值变量,记录state是否改变,初始值为false。然后声明一个nextState变量,同来存放下一次的state,初始化为一个空对象。
接着循环存放reducer对象键的数据,获取每个键名key,如果没有重新命名,这个key就是reducer函数的函数名;然后通过key在存放reducer的对象里取到对应的reducer;获取每个reducer对应的上一次的state值,保存到变量previousStateForKey中;然后通过当前reducer生成下一次的state
const nextStateForKey = reducer(previousStateForKey, action)
reducer里传入的state是上一次的state,所以说我们可以在reducer拿到上一次的state
接下来判断nextStateForKey的值是不是undefined,如果是则抛出错误,也就是说我们在reducer永远不要返回undefined。
getUndefinedStateErrorMessage函数就是用来生成报错信息的,源码如下:
function getUndefinedStateErrorMessage(key, action) {
//当action存在,将action.type的值赋值给actionType变量
const actionType = action && action.type
//action的描述
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.`
)
}
nextState[key] = nextStateForKey
将生成的state根据reducer的key保存到根state中,这边有点绕,等一下会举例说明。
接下来是一个判断,判断是否有reducer的state改变过
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
hasChanged的初始值为false,所以第一次是,肯定走后面的表达式,后面的表达式是判断上一次的state和下一次的state是否相等,当第一次循环后,hasChanged的值为true,所以下面的循环都不会走后面的表达式。
最后则是根据hasChanged来返回state,如果没有变化则返回原来的state。如果有变化则返回nextState。
以下面的例子来说明:
function user (state={},action) {
switch (action.type) {
//...
default:
return state
}
}
function menus(state{},action) {
switch (action.type) {
//...
default:
return state
}
}
const reducers = combineReducers({user,menus})
在这里我们拿到的reducers函数就是combineReducers返回的combination函数,它也是一个reducer函数,combination形成了一个闭包,闭包里存储了我们自己定义的reducer,以对象的形式存储。如下:
finalReducers = {
user: function user(state,action){...},
menus: function menus(state,action) {...}
}
当我们调用createStore函数传入combination函数时,里面会初始化执行一次combination函数,这时combination返回的值赋值给了createStore函数的currentState。根据combination函数的执行,我们知道其返回值应该是一个对象,对象的键是对应reducer函数的名,则为下面的值
nextState = {
user:{},
menus:{}
}
合并在一起的reducer所管理的state会放在同一个对象,key为reducer名(当然你也可以自己定义),这样我们就可以将reducer迭代划分无限层了。
在这个例子中最终的currentState就是nextState,你也可以将合并的reducer与其他reducer再次进行合并,currentState就是一棵树的根,下面可以划分很多枝,而枝上又可以有更小的枝。所以我们也将次称为store树。
下一篇:redux源码值applyMiddleware