前面的两个专栏我们分别对 Redux 应用中的唯一数据状态存储仓库 Store 以及其所存储的数据状态 State 和发起修改数据状态的 Action 以及避免冗余代码的 Action Creator 进行了详细的了解。在这个过程中,我们了解到在 Redux 中唯一修改数据状态 State 的方式是通过 dispatch 发送一个 action 到 reducer,然后在 reducer 中根据 action 的描述,对数据状态 State 做相应的修改,并返回一个新的 State。下面还是那张我们熟悉的 Redux 数据流程图:
Reducer
还记得我们在 Redux 系列二:Store 与 State 说到的关于 createStore 函数的源码吗?如果你忘了,可以点击链接回过头在温故一下:
export default function createStore(reducer, preloadedState, enhancer) {
// do something
}
createStore 函数会接受一到三个参数,第一个描述 State 修改逻辑的 reducer 函数是必传的,不然整个 Store 会无效:
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
在前面的内容中,我们也经常提到:reducer 是唯一修改 State 的逻辑,它会接受当前的 State 和修改 State 的动作 action 返回新的数据状态 newState,可以用伪代码表述为:
const reducer = (prevState, action) => {
// do something
return newState
}
对于 reducer 的基础概念有了认识,我们就来结合实际的场景说一下吧!
todo list 大家都应该很清楚吧,我们就说一下关于 todo list 的增删改查,用 reducer 大致可以描述成这样:
// reducer 逻辑
const reducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
reducer 的主逻辑是 switch case,我们会用 action.type 进行 case 条件匹配,然后在每个 case 分支返回新的数据状态 State,注意是返回新的 State 而不是在旧的 State 上进行修改,关于实现你可以借用扩展运算符(spread)和 Object.assign。上面 reducer 的逻辑都很简单,我就不做过多的介绍了。
不知道大家有没有注意到,我们一直在强调在 reducer 逻辑中,我们希望每次的逻辑是返回新的 State 而不是在旧的 State 上进行修改,这就是 reducer 的一个很强大的特性了:传函数。Redux 的作者希望开发者在设计 reducer 的时候,将它设计成一个纯函数,即有相同的输入即有相同的输出,这样在数据状态追踪方面有极大的好处。因为,每次都是生成新的数据状态 State,所以我们可以将每个阶段生成的 State 保存起来做回放或前进等方便问题追踪的功能和效果,还保证了 view 对应的 State 总是一个不变的对象。
Reducer 拆分与合并
上面我们给出的 reducer 的示例代码是极其简单的,但是在实际的业务场景开发中可能会有几十上百的页面逻辑,甚至更多。对于这么复杂的业务场景,State 对象肯定会很庞大,所有修改 reducer 的逻辑肯定不能写在一个 reducer 函数中,所以为了更好的进行管理我们可以对 reducer 进行拆分。比如有这样一个场景,在没进行拆分之前的代码是这样的:
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
})
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
})
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
})
default:
return state
}
}
我们不难看出,上面的示例代码糅杂了三个不同的业务场景:
// chatLog属性
ADD_CHAT
// statusMessage属性
CHANGE_STATUS
// userName属性
CHANGE_USERNAME
这三个属性之间没有任何的联系,我们完全可以把 reducer 函数进行拆分,使不同的函数负责处理不同属性,最后再把它们合并成一个大的 reducer 即可:
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
}
在这里,我们把上面那个大的 reducer 拆分成了三个小函数,让他们各自负责对应的状态属性。经过拆分后的 reducer 变得好读好管理多了,也和 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 reducer 完全可以对应。
为了更方便我们对 reducer 的拆分和合并,Redux 提供了 combineReducers 函数,你只要定义各个子 reducer 函数,然后用这个方法将它们合成一个大的 reducer。
import { combineReducers } from 'redux'
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
这种写法有一个前提,就是 State 的属性名必须与子 reducer 同名,如果不同名,就要采用下面的写法:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
// 等同于
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
总之,combineReducers() 做的就是根据各个小的 reducer 函数产生一个整体的 reducer 函数。该函数会根据 State 的 key 去执行相应的子 reducer,并将返回结果合并成一个大的 State 对象。
combineReducers
combineReducers 函数说到这里,我们可以推导一下函数的具体实现了。
我们说过 combineReducers 函数是用来将多个小的 reducer 合并成一个大的 reducer 的,所以首先 combineReducers 函数会接受多个小 reducer 函数的集合,形如前面示例代码提到的:
{
chatLog,
statusMessage,
userName
}
然后函数的执行会生成一个大的 reducer,而 reducer 又是一个函数类型,所以最终 combineReducers 函数的调用会返回一个函数。reducer 函数会接受当前的 state 和描述动作的 action 返回新的 state,所以 combineReducers 函数返回的函数会接受两个参数:一个是 state,另一个是 action。大致就应该是这样了:
const combineReducers = reducers => {
return (state = {}, action) => {
// do something
}
}
接着我们继续挖这个 do something 的逻辑。既然是进行多个 reducer 的合并,那 do something 的逻辑肯定是根据传入的 state 和 action 执行每个 reducer,然后将每个 reducer 返回的新的 state 进行合并,组成大的 state,最终的代码如下:
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
)
}
}
说了这么多,看看 Redux 中 combineReducers 函数的实现吧!删除一些注释和异常处理的代码,展示如下:
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 (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) {
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
}
}
其实基础逻辑和我们上面分析的一致,接受多个 reducer 的集合,返回一个接受 state 和 action 的函数,在函数逻辑中根据传入的 state 和 action 执行每个 reducer 函数,最后将每个 reducer 函数返回的新的 state 进行合并,返回大的 state 集合。
总结
说到这里,关于 reducer 的知识就讲完了。专栏开头,我们根据前面讲到的 Store、state 和 action 将 reducer 的话题引出来,然后讲到了 reducer 函数的基础逻辑设计,大型应用中 reducer 的拆分与合并,最后讲到了 reducer 合并函数 combineReducers 的实现与源码解析。还有关于 reducer 在一些特殊业务场景下需要替换的逻辑 replaceReducer 函数,由于在 Redux 系列二:Store 与 State 专栏已经讲到了,这里就不在赘述。关于后面一个专栏的内容,我希望将前面这几个专栏的知识点串起来,所以会结合 React 和 Redux 实现 todo list 的功能。