超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

combineReducers.js 源码阅读笔记

The combineReducers helper function turns an object whose values are different reducing functions into a single reducing function you can pass to createStore.

接口

combineReducers(reducers)

  • reducers (Object): reducers 是一个对象,它的值对应要混合的各个 reducer 。
  • return (Function): 多个独立的 reducer 合并后的最终的 finalReducers, 这个 finalReducers 的签名和 reducer 一样,接收 state 和action 做为参数,然后调用 所有 reducer 处理 action ,并把各个 reducer 处理 state 结果合成最终的新的 state ,并且返回的 state 的结构和传入的 reducers 参数对象的结构一致。

分析

当应用复杂起来以后,必然要对 reducer 进行拆分,否则全部放在一起的 reducer 难以维护。
而拆分后的 reducer 负责维护一部分的 state。

combineReducers 的作用就是把多个 reducer 的函数合并成一个最终的 reducer 函数,然后就可以使用这个函数管理 store,先看个示例:

//将 reducer 拆分成两个:
const todoReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text])
default:
return state
}
}

const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

//将拆分过的两个 reducer 合并
const combinedReducers = combineReducers({
todos: todoReducer,
counter: counterReducer
})

let store = createStore(combinedReducers)
console.log(store.getState())
//store 结构:{todos: [], counter: 0}

如上示例,我们调用 combinedReducers 得到的 store 的结构和 传递给 combinedReducers 的对象的结构一致,接下来我们修改 state:

store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
console.log(store.getState())
//store 结构:{todos: ['Use Redux'], counter: 0}

阅读代码后不难发现,combinedReducers 处理 state 可以认为是进行了以下操作:

store.dispatch(action);

store = {
todos: todoReducer(store[todos], action),
counter: todoReducer(store[counter], action),
...
others: othersReducer(store[others], action)
}

combineReducers 对传入的 reducer 约定了一些必须遵守的规则:

  • 如果接收到的 stateundefined ,必须对其初始化。
  • reducer 不允许返回 undefined,否则会抛出异常。
  • 如果 reducer 未匹配到传入的 action 则最好返回接收到的原始 store。
  • combineReducers 操作的 state 必须是普通对象,不能是其他的特殊对象,如 immutable 对象等。

当然不仅仅在使用 combineReducers 是遵守这些规则,使用自定义的类似的 工具函数的时候最好也要遵守这些习惯。

源码注释

源代码如下,加了阅读注释:

import { ActionTypes } from './createStore'
import isPlainObject from 'lodash/isPlainObject'
import warning from './utils/warning'

//action 没有 对应处理的 reducer 时候返回的错误信息
function getUndefinedStateErrorMessage(key, action) {
var actionType = action && action.type
var actionName = actionType && `"${actionType.toString()}"` || 'an action'

return (
`Given action ${actionName}, reducer "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state.`
)
}

// state 和 reducer 检查:
// 1. reducers 至少有一个成员 reducer,不能使空对象
// 2. inputState 必须是简单对象
// 3. inputState 中的属性,必须在 reducers 中存在同属性名的 reducer
function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
var reducerKeys = Object.keys(reducers)
var argumentName = action && action.type === ActionTypes.INIT ?
'preloadedState argument passed to createStore' :
'previous state received by the reducer'

// combineReducers 接收的参数对象至少需要又有一个 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.'
)
}

// inputState 是否是一个简单对象
// 简单对象是指 通过 "{}" 或者 "new Object" 创建的键值对的集合
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('", "')}"`
)
}

//判断 inputState 中是否存在 key ,在 reducers 中不存在
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.`
)
}
}


// reducers 合法性检查函数,主要检查要求以下几点:
// 1. 调用 reducer 返回值不允许为 undefined
// 2. reducer 在初次调用时,即被传入类型为 ActionTypes.INIT 的 action 需要对 state 初始化
// 3. 不要处理 redux/* 这个命名空间下的action 直接返回 currentState,
// 这一点代码未做检查,但是需要注意,自定义的 action type 最好不要用这个命名空间
function assertReducerSanity(reducers) {
Object.keys(reducers).forEach(key => {
var reducer = reducers[key]
var initialState = reducer(undefined, { type: ActionTypes.INIT })

// 此处检查 reducer 是否处理值为 undefined 的 state,如果未处理,即 reducer 返回 undefined 抛出异常
// 按照约定,当传递给reducer的state为 undefined 时,通常在初始化的时候,reducer第一次被调用时,
// state 为 undefined, 此时 reducer 需要给 state 一个默认值, 而不能返回 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.`
)
}

// 声明一种随机的 action type,确保不会有人使用这种action
// 使用这么奇怪的 action 是为了检查,当传入 reducer 不认识的 action type 时,reducer 会不应该返回 undefined。
// 实际上,按照约定,当传入不认识的 action type 或者想要忽视的action type 时,返回 current state 即可
// 同时,警告信息还指出不应该处理任何 redux/* ,命名空间下的 action,除非 current state 为 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.`
)
}
})
}

/**
* 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) {
var reducerKeys = Object.keys(reducers)
var finalReducers = {}

// 把多个 reducers 合并到 finalReducers 对象中
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
var finalReducerKeys = Object.keys(finalReducers)

// 对 reducers 合法性进行检查
var sanityError
try {
assertReducerSanity(finalReducers)
} catch (e) {
sanityError = e
}

return function combination(state = {}, action) {
// reducers 合法性检查存在异常抛出
if (sanityError) {
throw sanityError
}


// state 和 reducer 检查:
// 1. reducers 至少有一个成员 reducer,不能使空对象
// 2. inputState 必须是简单对象
// 3. inputState 中的属性,必须在 reducers 中存在同属性名的 reducer
// 非生产环境下打印警告日志
if (process.env.NODE_ENV !== 'production') {
var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
if (warningMessage) {
warning(warningMessage)
}
}

// 最终 nextState 对象的结构和 finalReducers 的结构一致
var hasChanged = false
var nextState = {}
// 遍历 finalReducers ,为 finalReducers 的每个成员调用 reducer(subState, action)
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}

相关

Redux 源码阅读笔记:

参考

本文阅读代码版本 3.5.2