Redux Reducer 逻辑拆分:函数式分解与 Reducer 组合的术语体系与实战模式
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 社区交流、代码评审和文档阅读中都会被反复使用务必准确掌握reducerreducer 函数任何具有(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 函数例如combineReducers、redux-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在初始化时通过assertReducerShapesrc/combineReducers.ts#L62-L94用ActionTypes.INIT和随机 action 探测每个 reducer运行期若返回undefined会直接抛错。此外combineReducers可以在 reducer 层级中的任何一层使用不限于顶层——子 reducer 变得复杂时可以继续用combineReducers把它再拆分成孙 reducer如此递归下去。实战从一个巨型 Reducer 重构为组合结构下面通过官方 重构 Reducer 示例 的完整过程直观展示三类拆分方式如何层层递进地应用。该示例刻意写得冗余目的是演示概念与重构过程。第一步最初的巨型 reducer一个同时处理可见性过滤与待办事项的appReducerconst 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_TODO与EDIT_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 Boilerplatefunction 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 } // 切片 reducerslice reducer const visibilityReducer createReducer(SHOW_ALL, { SET_VISIBILITY_FILTER: setVisibilityFilter }) // 用例函数case reducer function addTodo(todosState, action) { /* ... */ } function toggleTodo(todosState, action) { /* ... */ } function editTodo(todosState, action) { /* ... */ } // 切片 reducerslice reducer const todosReducer createReducer([], { ADD_TODO: addTodo, TOGGLE_TODO: toggleTodo, EDIT_TODO: editTodo }) // 根 reducerroot reducer const appReducer combineReducers({ visibilityFilter: visibilityReducer, todos: todosReducer })至此我们得到了各种拆分形态的完整样本updateObject、createReducer是工具函数setVisibilityFilter、addTodo等是针对具体 case 的用例函数visibilityReducer、todosReducer是切片 reducer而appReducer则是根 reducer。最终代码虽然比原版更长主要因为工具函数提取、注释与刻意冗余但每个函数的职责范围都更小、意图更清晰在真实项目中这些函数通常还会进一步拆到reducerUtilities.js、visibilityReducer.js、todosReducer.js、rootReducer.js等独立文件中。术语落地对照本仓库的 todos 示例本仓库的 examples/todos 示例是这套术语体系的真实落地。其中 todos.js 是一个标准的slice reducer管理todos切片对ADD_TODO、TOGGLE_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 中携带跨切片数据或使用compose把undoReducer、filterReducer等包装在切片 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),仅供参考

相关新闻

低功耗段码LCD驱动芯片TM16C21:原理、设计与实战排坑

低功耗段码LCD驱动芯片TM16C21:原理、设计与实战排坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 1:24:37 阅读更多 →
iPhone Duo双屏传闻下,Swift开发者必须掌握的多屏适配与文件操作

iPhone Duo双屏传闻下,Swift开发者必须掌握的多屏适配与文件操作

这阵子社区里讨论度最高的硬件传闻,除了新机型的常规升级,就是一台被大家戏称为 iPhone Duo 的双屏设备。虽然苹果官方一个字都没确认,但作为常年泡在 Swift 生态里的人,我明显感觉到周边讨论已经从"会不会出"转向了&qu…

2026/9/18 18:41:25 阅读更多 →
CANN ops-nn 算子库安全声明解读:权限最小化、文件管控与构建运行安全实践

CANN ops-nn 算子库安全声明解读:权限最小化、文件管控与构建运行安全实践

CANN ops-nn 算子库安全声明解读:权限最小化、文件管控与构建运行安全实践 【免费下载链接】ops-nn 本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。 项目地址: https://gitcode.com/cann/ops-nn CANN ops-nn 是 CANN 提供的神…

2026/9/20 18:19:06 阅读更多 →

最新新闻

SpringBoot三层架构实战:从零实现用户管理系统

SpringBoot三层架构实战:从零实现用户管理系统

1. 项目概述:SpringBoot三层架构实战刚入行Java开发时,总听前辈们念叨"三层架构",但真正自己动手实现一个完整的用户管理系统才发现,理论到实践之间藏着不少门道。这次就用SpringBoot从零实现带三层架构的用户增删改查&…

2026/9/21 2:01:05 阅读更多 →
AI基础知识核心框架:从机器学习到大模型的应用与学习路径

AI基础知识核心框架:从机器学习到大模型的应用与学习路径

简介:这是一份面向人工智能初学者的入门级PPT讲义,共61页,系统梳理AI的核心概念与基础知识。内容从人工智能的定义、关键点、智能维度出发,清晰介绍符号主义、联结主义、行为主义等主要学派,并依据智能水平区分弱人工智…

2026/9/21 2:01:05 阅读更多 →
二进制与十进制互转全解析:整数、小数、负数及精度处理

二进制与十进制互转全解析:整数、小数、负数及精度处理

1. 为什么二进制和十进制互转值得单独拿出来讲很多人第一次接触进制转换,是在计算机基础课上。老师写一个除2取余的竖式,再写一个按权展开的多项式,然后说“记住就行”。结果到了实际用的时候,比如看内存地址、分析协议报文、处理…

2026/9/21 2:01:05 阅读更多 →
Si3N4与SiNx有什么区别?芯片制造中两种氮化硅的工艺差异详解

Si3N4与SiNx有什么区别?芯片制造中两种氮化硅的工艺差异详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 2:01:05 阅读更多 →
大型汽车集团数智化战略规划:145页PPT框架拆解与实操落地

大型汽车集团数智化战略规划:145页PPT框架拆解与实操落地

简介:某大型汽车集团数字化转型数智化战略规划设计方案PPT,聚焦“互联网1354”顶层战略框架,面向企业数字化战略规划人员、咨询顾问、汽车行业管理者及对转型顶层设计感兴趣的从业者。压缩包内含单个145页PPT文件,约26.35MB&#…

2026/9/21 2:01:05 阅读更多 →
美团数据分析手册拆解:指标体系、SQL与归因实战

美团数据分析手册拆解:指标体系、SQL与归因实战

简介:这份《美团数据分析手册》是一份面向数据分析初级与进阶学习者的业务实战指南,聚焦外卖、到店、酒旅、出行、金融、闪购等核心业务线,系统讲解如何构建指标体系、应用数据分析方法论并支撑业务决策。资源为单个PDF文件,仅1.1…

2026/9/21 2:00:05 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/20 0:00:46 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/20 0:00:46 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/19 23:01:36 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →