Redux Reducer 逻辑拆分:函数式分解与 Reducer 组合的术语体系与实战模式
2026/9/18 18:42:07 网站建设 项目流程

Redux Reducer 逻辑拆分:函数式分解与 Reducer 组合的术语体系与实战模式

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

对于任何有一定规模的应用程序,把全部状态更新逻辑塞进一个 reducer 函数,很快就会变得难以维护。本文基于本仓库的《Structuring Reducers》系列文档,系统梳理 Redux 中拆分 reducer 逻辑的三类函数模式、一整套精确的术语体系(root reducer、slice reducer、case function、higher-order reducer 等),并结合本仓库combineReducers的源码实现与 todo 示例,讲解函数式分解(Functional Decomposition)Reducer 组合(Reducer Composition)这两种最核心的组织方式。读完本文,你将能够为自己的应用设计出清晰、可维护、可复用的 reducer 结构,并理解combineReducers在底层究竟做了什么。

为什么要把 Reducer 逻辑拆开

Redux 的 reducer 本质上是"只是"一个函数:它以(previousState, action) => newState为签名,必须保持纯函数与可预测性(详见 Redux Fundamentals 第三部分)。除此之外,Redux 并不关心你在 reducer 内部如何组织逻辑——这是自由的来源,也是混乱的来源。

关于函数应该多长,并没有统一的标准,但业界普遍认同:函数应当相对短小,并且最好只做一件特定的事情。因此,把非常长或职责过多的代码块拆成更小、更容易理解的片段,是良好的编程实践。由于 Redux 的 reducer 就是一个普通函数,同样的原则自然适用:

你可以把一部分 reducer 逻辑拆到另一个函数里,然后从父函数调用它。

这一点是整个 reducer 组织方式的基石:拆分不需要任何框架机制,只是普通的 JavaScript 函数拆分。

拆分后的函数通常属于三类

文档将拆分出来的新函数归纳为三类典型形态,理解它们的差异是掌握 reducer 架构的第一步:

类别特征典型签名示例
1. 小工具函数(utility functions)包含一段可复用的逻辑,可能在多个地方用到,不一定与具体业务相关任意updateObject(old, newValues)updateItemInArray(array, id, cb)
2. 特定更新场景的处理函数处理某个具体的更新场景,往往需要除(state, action)之外的其他参数(state, action, extra)需要额外传入state.b作为第三参数的函数
3. 某个状态切片的全部更新处理函数处理给定状态切片(slice)的所有更新通常是标准的(state, action)todosReducer(state.todos, action)

需要特别注意的是:第 3 类函数是 Redux 中最常用的组织方式,但它并不是唯一可行的方式。实际上,将三种方式混合使用通常是更好的选择——重构 Reducer 示例 就展示了三种方式并用的完整过程。

术语体系:区分不同类型的 Reducer 函数

为了在讨论中清晰地区分不同用途的函数,官方文档定义了一套精确的术语。这套术语在 Redux 社区交流、代码评审和文档阅读中都会被反复使用,务必准确掌握:

  • reducer(reducer 函数):任何具有(state, action) -> newState签名的函数——即任何"可以作为Array.prototype.reduce回调参数"的函数。这是最宽泛的定义。
  • root reducer(根 reducer):真正作为第一个参数传给createStore的那个 reducer 函数。它是整个 reducer 逻辑中唯一必须具备(state, action) -> newState签名的部分。
  • slice reducer(切片 reducer):用于处理状态树中某一个特定切片的 reducer,通常通过传给combineReducers来实现。
  • case function(用例函数):用于处理某个特定 action 的更新逻辑的函数。它可能是一个标准的 reducer 函数,也可能需要其他参数才能正确工作。
  • higher-order reducer(高阶 reducer):接收一个 reducer 函数作为参数,和/或返回一个新的 reducer 函数(例如combineReducersredux-undo)。
  • sub-reducer(子 reducer):一些讨论中用它泛指任何"不是根 reducer"的函数,但这个术语并不精确。
  • business logic(业务逻辑)与 utility functions(工具函数):也有人用这对概念来区分"与应用特定行为相关的函数"和"与应用无关的通用函数"。

这套术语的价值在于:当你说"这里用了一个 slice reducer,那里是一个 case function"时,听者能立刻理解函数的职责边界,而无需阅读完整实现。

从函数式分解到 Reducer 组合

把复杂过程拆成更小、更易理解的部分,在软件工程中被称为函数式分解(Functional Decomposition),它可以应用于任何代码。而在 Redux 中,使用上述第 3 种方式——即"根据状态切片把更新逻辑委托给其他函数"——极其常见,Redux 官方将这种结构方式命名为Reducer 组合(Reducer Composition),它也是目前使用最广泛的 reducer 逻辑组织方式。

由于这种方式太普遍,Redux 专门内置了一个工具函数combineReducers()来抽象"按状态切片委托工作"的过程:

import { combineReducers } from 'redux' const rootReducer = combineReducers({ todos: todosReducer, visibilityFilter: visibilityReducer })

但必须强调:combineReducers不是唯一可用的模式。正如文档所说,"完全有可能把三种拆分方式全部用于拆分逻辑,而且这通常也是个好主意"。

源码视角:combineReducers到底做了什么

为了真正理解 reducer 组合,值得看一下本仓库中combineReducers的实现(src/combineReducers.ts)。它是典型的高阶 reducer:接收一个"值为 reducer 函数"的对象,返回一个新的 reducer。

其核心逻辑(src/combineReducers.ts#L157-L200)可以概括为:返回的组合函数接收整个 state 与 action,然后遍历每一个 slice reducer,把对应切片的状态和当前 action 传给它,收集各切片返回的新状态,拼装成新的 state 对象:

return function combination(state = {}, action) { // ...开发环境下检查 state 形状、抛错等 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') { // 抛错:slice reducer 对某个 action 返回了 undefined } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length return hasChanged ? nextState : state }

从这个实现可以验证几个关键结论:

  • "dispatch 一个 action 时 Redux 会调用所有 reducer"吗?严格地说,只有一个根 reducer 函数,所以默认答案是"不会"。但combineReducers的行为恰恰是"调用它包装的所有 slice reducer",让每个切片都有机会响应并更新自己的状态(详见 UsingcombineReducers)。
  • 未识别 action 必须返回原状态:组合函数通过hasChanged与引用比较来判定状态是否变化,只有某个切片返回了不同引用时才生成新 state 对象,这正是"未识别 action 返回原 state"这一规则的价值所在。
  • 每个 slice reducer 都不能返回undefinedcombineReducers在初始化时通过assertReducerShape(src/combineReducers.ts#L62-L94)用ActionTypes.INIT和随机 action 探测每个 reducer;运行期若返回undefined会直接抛错。

此外,combineReducers可以在 reducer 层级中的任何一层使用,不限于顶层——子 reducer 变得复杂时,可以继续用combineReducers把它再拆分成"孙 reducer",如此递归下去。

实战:从一个巨型 Reducer 重构为组合结构

下面通过官方 重构 Reducer 示例 的完整过程,直观展示三类拆分方式如何层层递进地应用。(该示例刻意写得冗余,目的是演示概念与重构过程。)

第一步:最初的巨型 reducer

一个同时处理"可见性过滤"与"待办事项"的appReducer

const initialState = { visibilityFilter: 'SHOW_ALL', todos: [] } function appReducer(state = initialState, action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': { return Object.assign({}, state, { visibilityFilter: action.filter }) } case 'ADD_TODO': { return Object.assign({}, state, { todos: state.todos.concat({ id: action.id, text: action.text, completed: false }) }) } case 'TOGGLE_TODO': { return Object.assign({}, state, { todos: state.todos.map(todo => { if (todo.id !== action.id) { return todo } return Object.assign({}, todo, { completed: !todo.completed }) }) }) } case 'EDIT_TODO': { return Object.assign({}, state, { todos: state.todos.map(todo => { if (todo.id !== action.id) { return todo } return Object.assign({}, todo, { text: action.text }) }) }) } default: return state } }

这个函数虽然不长,但已经过于复杂:混杂了两个不同领域的关注点(过滤 vs 待办列表),嵌套让更新逻辑难以阅读。

第二步:提取工具函数(第 1 类函数)

先抽出"返回更新后新对象"与"更新数组中特定项"这两个可复用工具函数:

function updateObject(oldObject, newValues) { // 把"新对象作为 Object.assign 第一个参数"的想法封装起来, // 确保正确复制数据而不是原地修改 return Object.assign({}, oldObject, newValues) } function updateItemInArray(array, itemId, updateItemCallback) { const updatedItems = array.map(item => { if (item.id !== itemId) { // 只更新一项,其余保持原样 return item } // 用回调创建更新后的项 const updatedItem = updateItemCallback(item) return updatedItem }) return updatedItems }

改造后的appReducer中,TOGGLE_TODOEDIT_TODO不再重复书写"遍历数组 + 拷贝对象"的样板代码。

第三步:提取用例函数(第 2/3 类函数)

把每个具体 case 拆成独立函数(此时它们仍是(state, action)签名):

function setVisibilityFilter(state, action) { return updateObject(state, { visibilityFilter: action.filter }) } function addTodo(state, action) { const newTodos = state.todos.concat({ id: action.id, text: action.text, completed: false }) return updateObject(state, { todos: newTodos }) } function toggleTodo(state, action) { const newTodos = updateItemInArray(state.todos, action.id, todo => { return updateObject(todo, { completed: !todo.completed }) }) return updateObject(state, { todos: newTodos }) } function editTodo(state, action) { const newTodos = updateItemInArray(state.todos, action.id, todo => { return updateObject(todo, { text: action.text }) }) return updateObject(state, { todos: newTodos }) } function appReducer(state = initialState, action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': return setVisibilityFilter(state, action) case 'ADD_TODO': return addTodo(state, action) case 'TOGGLE_TODO': return toggleTodo(state, action) case 'EDIT_TODO': return editTodo(state, action) default: return state } }

现在每个 case 里发生了什么一目了然,模式也开始浮现。

第四步:按领域切片拆分(Reducer 组合的雏形)

让过滤逻辑与待办逻辑彻底分离。注意:因为每个切片 reducer 只拿到自己的那部分 state,它们不再需要返回复杂的嵌套对象,因此变得更加简单:

function setVisibilityFilter(visibilityState, action) { // 其实我们根本不关心旧状态 return action.filter } function visibilityReducer(visibilityState = 'SHOW_ALL', action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': return setVisibilityFilter(visibilityState, action) default: return visibilityState } } function addTodo(todosState, action) { const newTodos = todosState.concat({ id: action.id, text: action.text, completed: false }) return newTodos } function toggleTodo(todosState, action) { const newTodos = updateItemInArray(todosState, action.id, todo => { return updateObject(todo, { completed: !todo.completed }) }) return newTodos } function editTodo(todosState, action) { const newTodos = updateItemInArray(todosState, action.id, todo => { return updateObject(todo, { text: action.text }) }) return newTodos } function todosReducer(todosState = [], action) { switch (action.type) { case 'ADD_TODO': return addTodo(todosState, action) case 'TOGGLE_TODO': return toggleTodo(todosState, action) case 'EDIT_TODO': return editTodo(todosState, action) default: return todosState } } function appReducer(state = initialState, action) { return { todos: todosReducer(state.todos, action), visibilityFilter: visibilityReducer(state.visibilityFilter, action) } }

第五步:用createReducer消除 switch 样板

很多人不喜欢switch,可以用查表式createReducer替代(详见 Reducing Boilerplate):

function createReducer(initialState, handlers) { return function reducer(state = initialState, action) { if (handlers.hasOwnProperty(action.type)) { return handlersaction.type } else { return state } } } const visibilityReducer = createReducer('SHOW_ALL', { SET_VISIBILITY_FILTER: setVisibilityFilter }) const todosReducer = createReducer([], { ADD_TODO: addTodo, TOGGLE_TODO: toggleTodo, EDIT_TODO: editTodo })

第六步:用combineReducers收尾

最后,用 Redux 内置的combineReducers接管顶层"按切片分发"的逻辑:

// 可复用的工具函数 function updateObject(oldObject, newValues) { /* ... */ } function updateItemInArray(array, itemId, updateItemCallback) { /* ... */ } function createReducer(initialState, handlers) { /* ... */ } // 用例函数(case reducer) function setVisibilityFilter(visibilityState, action) { return action.filter } // 切片 reducer(slice reducer) const visibilityReducer = createReducer('SHOW_ALL', { SET_VISIBILITY_FILTER: setVisibilityFilter }) // 用例函数(case reducer) function addTodo(todosState, action) { /* ... */ } function toggleTodo(todosState, action) { /* ... */ } function editTodo(todosState, action) { /* ... */ } // 切片 reducer(slice reducer) const todosReducer = createReducer([], { ADD_TODO: addTodo, TOGGLE_TODO: toggleTodo, EDIT_TODO: editTodo }) // 根 reducer(root reducer) const appReducer = combineReducers({ visibilityFilter: visibilityReducer, todos: todosReducer })

至此,我们得到了各种拆分形态的完整样本:updateObjectcreateReducer是工具函数,setVisibilityFilteraddTodo等是针对具体 case 的用例函数,visibilityReducertodosReducer是切片 reducer,而appReducer则是根 reducer。最终代码虽然比原版更长(主要因为工具函数提取、注释与刻意冗余),但每个函数的职责范围都更小、意图更清晰;在真实项目中,这些函数通常还会进一步拆到reducerUtilities.jsvisibilityReducer.jstodosReducer.jsrootReducer.js等独立文件中。

术语落地:对照本仓库的 todos 示例

本仓库的 examples/todos 示例是这套术语体系的真实落地。其中 todos.js 是一个标准的slice reducer(管理todos切片,对ADD_TODOTOGGLE_TODO做出响应),visibilityFilter.js 是另一个 slice reducer,而 index.js 则是典型的root reducer构建方式:

import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter })

注意这里combineReducers的 key 名决定了最终 state 的形状——store.getState()将返回{ todos, visibilityFilter }。这正是combineReducers"根据传入对象的 key 命名状态切片"的行为,详见 combineReducers API 文档 与 UsingcombineReducers中关于对象字面量简写可能带来的命名困惑的讨论。

边界:combineReducers不做什么

需要清醒认识到,combineReducers刻意只解决一个常见场景:把"普通 JS 对象构成的状态树"按切片委托给各自的 slice reducer。它不处理以下情况(详见 BeyondcombineReducers):

  • 状态树由 Immutable.js 的 Map 构成;
  • 需要把状态树的其他部分作为额外参数传给某个 slice reducer(即跨切片共享数据);
  • 需要对 slice reducer 的调用顺序做特殊控制。

当超出combineReducers的核心用例时,正确做法是使用更自定义的 reducer 逻辑:例如手写一个组合函数,在特定 action 下给sliceReducerA传入state.b作为第三参数;或借助 thunk 在 action 中携带跨切片数据;或使用composeundoReducerfilterReducer等包装在切片 reducer 外层。关键在于:reducer 只是函数,combineReducers只是工具箱里的一件工具——函数可以包含 switch 之外的条件逻辑,可以互相组合包装,也可以互相调用。

小结

拆分 reducer 逻辑没有魔法:它是普通的函数拆分,配上 Redux 特定的组织习惯。掌握本文的术语体系(reducer、root reducer、slice reducer、case function、higher-order reducer)与三种拆分方式(工具函数、用例函数、切片 reducer),再理解combineReducers的源码行为与边界,你就能在任何规模的 Redux 应用中设计出结构清晰、易于测试和复用的 reducer 层。完整的系列文档从 Structuring Reducers 总览 开始,还包括 Immutable Update Patterns、Normalizing State Shape、Reusing Reducer Logic 等进阶主题,可作为后续深入阅读的路线图。

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询