首先我们先看一下utils目录下的几个工具函数。
src/utils/pick.js
export default function pick(obj, fn) {
return Object.keys(obj).reduce((result, key) => {
if (fn(obj[key])) {
result[key] = obj[key]
}
return result
}, {})
}
这个函数很像Array.prototype#filter
,不过它是为对象准备的,作用是给每一个value作筛选,如果value没有通过校验,就舍弃响应的key,最后返回新生成的对象。
src/utils/mapValues.js
export default function mapValues(obj, fn) {
return Object.keys(obj).reduce((result, key) => {
result[key] = fn(obj[key], key)
return result
}, {})
}
这个函数也是对一个对象做遍历,然后把value以及key传入参数中的函数fn
里,并把结果作为value,和当前key存到一个新对象中,最后返回这个对象。
src/combineReducers.js
这个文件一开始定义了几种错误情况的Error/Warning Message.
function getUndefinedStateErrorMessage(key, action) {
var actionType = action && action.type
var actionName = actionType && `"${actionType.toString()}"` || 'an action'
return (
`Reducer "${key}" returned undefined handling ${actionName}. ` +
`To ignore an action, you must explicitly return the previous state.`
)
}
当一个reducer对于一个action没有返回state的时候,会抛出这个错误,所以在reducer的switch语句里,用于处理未知type的default
条件应该返回当前state。
function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
var reducerKeys = Object.keys(reducers)
var argumentName = action && action.type === ActionTypes.INIT ?
'initialState 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('", "')}"`
)
}
var unexpectedKeys = Object.keys(inputState).filter(key => !reducers.hasOwnProperty(key))
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.`
)
}
}
这里是每次reducer操作state时进行的一些校验,并输出响应的message,进行的校验有:
- 检测store中reducer的数量,如果没有reducer的话抛出错误信息;
- 作为reducer参数的state必须是朴素对象,否则会抛出错误信息;
- state中的key应该全都也全都有相应的reducer,否则抛出错误信息。
function assertReducerSanity(reducers) {
Object.keys(reducers).forEach(key => {
var reducer = reducers[key]
var 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.`
)
}
var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
if (typeof reducer(undefined, { type }) === '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.`
)
}
})
}
这段代码主要是校验reducer是否进行了异常处理,也就是说,对于参数中的action的type字段,如果是系统保留的@@redux/INIT
或者未知常量,那么reducer应该返回当前state,而不应该出现undefined的情况。
export default function combineReducers(reducers) {
var finalReducers = pick(reducers, (val) => typeof val === 'function')
var sanityError
try {
assertReducerSanity(finalReducers)
} catch (e) {
sanityError = e
}
暴露一个combineReducers
函数,接收一个reducer数组,并且使用上文中的pick
函数对数组内容进行校验,也就是reducer的类型必须是一个函数,并且把校验过的对象保存为finalReducers
。
return function combination(state = {}, action) {
if (sanityError) {
throw sanityError
}
if (process.env.NODE_ENV !== 'production') {
var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
if (warningMessage) {
console.error(warningMessage)
}
}
combineReducers方法返回一个combination
函数, 这个函数才是reducer处理state的关键,在这个函数的开始,会判断reducer是否有校验错误,有的话抛出,然后进行第二段代码中的校验,并且打印出错误信息。
var hasChanged = false
var finalState = mapValues(finalReducers, (reducer, key) => {
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
return nextStateForKey
})
return hasChanged ? finalState : state
}
}
这个部分的主体就是上面说的mapValues
函数,被传入函数fn
的参数是finalReducers对象中的键(reducer函数名)和值(reducer函数内容),然后从state中以reducer函数名作为key获得这个reducer应该处理的state即previousStateForKey
,并且把state和action作为参数执行reducer函数,获得处理后的state即nextStateForKey
。然后根据处理前和处理后的state进行对比来判断store里的内容是否进行了改变,再把nextStateForKey
作为value存入结果对象finalState
中。最后根据是否有state改变来选择返回原有state还是处理后的finalState
。
这里需要注意的有几点:
- Redux并不知道store里state的哪个分支该由哪个reducer来处理,而是对每一个reducer都用相同的action执行一遍,所以在一个项目里的action最好不要有重名的type,否则每次两个reducer都会同时执行。
- 正因为上一点的存在,Redux里dispatch一个action的时候,其实每个reducer都会响应这个action,所以一个reducer对于未知的type一定要有异常处理,即返回当前state,返回undefined会抛异常。
- state应该是immutable的,reducer对于state的任何操作都应该生成一个新对象,而不是在旧的对象上修改,这样Redux就可以简单的通过
nextStateForKey !== previousStateForKey
来判断state是否有改变。
到这里,combineReducers.js这个文件的讲解也就结束了,Redux源码中代码量最大的两个文件就这样搞定了,可以看出其实Redux的代码量是很小的,而且用了一些约定(比如immutable state)来让程序更加简洁优美。
这里还有一个小tip,就是在JavaScript中,当用花括号来包裹基本变量时,会自动以字面量的形式创建一个对象,combineReducers函数就是这样创建以reducer函数名为key函数内容为value的对象的。比如:
> a = 'test'
'test'
> b = (test) => console.log(test)
[Function]
> c = {a, b}
{ a: 'test', b: [Function] }