Redux 核心机制详解:State、Actions 与 Reducers 的设计、编写与 combineReducers 原理
本文基于 Redux 官方教程 Fundamentals 系列的 Part 3,系统讲解 Redux 三大基石——State(状态)、Actions(动作)与 Reducers(归约函数):如何把业务需求转化为纯 JS 数据结构的 state,如何设计描述"发生了什么"的 action 对象,如何编写严格遵循"纯函数 + 不可变更新"规则的 reducer 函数,以及如何把庞大的根 reducer 拆分为按"feature"组织的 slice 文件,再用 combineReducers 组装回单一根 reducer。文章会对照当前仓库 redux 5.0.1 的源码(src/combineReducers.ts、src/createStore.ts 等),解释状态初始化、action 校验与"未变化则短路"等底层机制。读完本文,你将能够独立设计 state 结构、写出合规的 reducer 与 action 清单,并理解 combineReducers 在运行时到底做了什么检查。
Redux 5.x 的核心包由 src/index.ts 统一导出,主要 API 包括 combineReducers(本文重点)、applyMiddleware、bindActionCreators、compose 以及(已被标记弃用的)createStore;对应的类型定义位于 src/types/reducers.ts 与 src/types/actions.ts。
前置知识与学习项目准备
Part 2 已建立的背景
Redux 的价值在于为全局应用状态提供一个唯一的中央存放点(store)。围绕它有两个核心概念:
- Dispatching(分发):通过
dispatch派发 action 对象,这是改变状态的唯一途径; - Reducer 函数:接收当前 state 与 action,返回新的 state。
Part 3 的任务就是把这两个概念落地为可运行的代码。
教程配套工程与启动方式
官方教程配套了一个预配置的示例工程(React + 内置样式 + 一个假 REST API),你可以按以下方式使用:
- 在 CodeSandbox 中打开并 fork 官方嵌入的
redux-fundamentals-example-app工程; - 也可以克隆对应的
reduxjs/redux-fundamentals-example-app仓库,然后执行npm install安装依赖、npm start启动项目; - 如果只想看最终成品,官方提供了
tutorial-steps分支可对照学习。
完成本教程后,官方推荐使用 Redux 的 Create-React-App 模板(reduxjs/cra-template-redux)创建新项目,它已预装 Redux Toolkit 与 React-Redux,内置一个"现代化改造版"的 counter 示例(即 Part 1 中看到的那个示例)。不借助模板从零搭建时,步骤如下:
- 安装
@reduxjs/toolkit和react-redux两个包; - 使用 RTK 的
configureStoreAPI 创建 store,并传入至少一个 reducer 函数; - 在应用入口文件(如
src/index.js)中导入 Redux store; - 用 React-Redux 的
<Provider>组件包裹根组件:
root.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
注意版本现状:本仓库当前 package.json 中
redux的版本为 5.0.1。在 5.x 中,createStore已被标记为@deprecated,官方推荐改用 Redux Toolkit 的configureStore(详见 src/createStore.ts 中的弃用注释,以及 docs/introduction/why-rtk-is-redux-today.md)。本教程为了讲清底层原理,仍以createStore为主,但本文所有 state/action/reducer 设计原则与combineReducers机制在configureStore下完全一致——RTK 的configureStore实际上会自动调用combineReducers。
初始工程结构速览
教程示例工程基于 Vite 标准模板改造,/src 目录下的关键文件:
index.js:应用入口,渲染主<App>组件;App.js:主应用组件;index.css:全局样式;/api:client.js是一个fetch的轻量封装,支持 GET/POST;server.js提供假的 REST API 端点,后续章节会用到;/exampleAddons:存放教程后面会用来演示的 Redux 附加组件。
定义 Todo 示例应用的需求
教程用一个经典的 Todo 应用来串联 state/action/reducer 的全部知识点,因为 Todo 应用能覆盖真实应用中最常见的几类操作:维护一个条目列表、处理用户输入、数据变化时刷新 UI。
初始业务需求如下:
- UI 由三个主要区域组成:
- 一个输入框,让用户输入新 Todo 条目的文本;
- 一个展示所有已有 Todo 条目的列表;
- 一个底部区域,显示未完成的 Todo 数量,并提供筛选选项。
- 列表条目应带复选框,可切换"completed"状态;还应能为条目从预定义颜色列表中选择一个颜色分类标签,以及支持删除条目;
- 计数器应根据未完成任务数量做单复数变化:"0 items"、"1 item"、"3 items";
- 应有两个按钮:把全部 Todo 标记为已完成、清除(删除)所有已完成的 Todo;
- 提供两种筛选方式:
- 按 "All" / "Active" / "Completed" 筛选;
- 按一个或多个颜色筛选,显示标签颜色匹配的 Todo。
最终效果如官方截图所示:
设计 State 值与状态结构
React 与 Redux 的一条核心原则是:UI 应该基于 state 构建。因此设计应用的一个有效思路是,先枚举出描述应用行为所需的全部 state,并尽可能用"最少的值"来描述 UI——state 越少,需要维护和更新的数据就越少。
从需求中提炼 state
概念上,这个应用有两块主要 state:
- 当前的 Todo 条目列表本身;
- 当前的筛选项。
另外还需要记录用户在"Add Todo"输入框里正在输入的内容,但这块相对次要,教程留到后面再处理。
每个 Todo 条目需要存储:
- 用户输入的文本;
- 表示是否完成的布尔标志;
- 一个唯一 ID;
- 一个颜色分类(如果选择了的话)。
筛选行为可以用枚举值描述:
- 完成状态:"All"、"Active"、"Completed";
- 颜色:"Red"、"Yellow"、"Green"、"Blue"、"Orange"、"Purple"。
从这些值还可以看出:Todos 属于 app state(应用处理的核心数据),而筛选值属于 UI state(描述应用当前正在做什么的状态)。区分这两类 state 有助于理解它们各自的用途。
根状态结构示例
在 Redux 中,应用状态永远保存在纯 JavaScript 对象和数组里。这意味着 state 中不能放:类实例、Map / Set / Promise / Date 这类内置 JS 类型、函数,或任何非纯 JS 数据。
Redux 的根 state 值几乎总是一个纯 JS 对象,其他数据嵌套其中。据此,本应用的 state 结构为:
- 一个 Todo 条目对象数组,每个条目包含:
id:唯一数字;text:用户输入的文本;completed:布尔标志;color:可选的颜色分类;
- 筛选选项:
- 当前的 "completed" 筛选值;
- 当前选中的颜色分类数组。
完整的 state 示例:
const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'Active',
colors: ['red', 'blue']
}
}
需要特别强调:在 Redux 之外拥有其他 state 值是完全允许的! 本例目前足够小,所有状态都放在了 Redux store 里,但正如后续教程会看到的,有些数据并不需要进 Redux(例如"这个下拉框是否打开"、"表单输入框的当前值")。
设计 Actions
Action 是带 type 字段的纯 JS 对象。可以把一个 action 理解为一个"事件",描述应用中刚刚发生的事情。就像基于需求设计 state 结构一样,也可以列出描述"会发生什么"的 action 清单:
- 基于用户输入的文本添加一个新的 Todo 条目;
- 切换某个 Todo 的 completed 状态;
- 为某个 Todo 选择颜色分类;
- 删除一个 Todo;
- 把所有 Todo 标记为已完成;
- 清除所有已完成的 Todo;
- 更换 "completed" 筛选值;
- 新增一个颜色筛选;
- 移除一个颜色筛选。
描述"发生了什么"所需的额外数据,通常放在 action.payload 字段中——它可以是数字、字符串,或包含多个字段的对象。
Redux store 并不关心 action.type 的实际字符串是什么,但你自己的代码会靠 action.type 判断是否需要更新;调试时你也经常会在 Redux DevTools 扩展里查看这些 type 字符串。所以action type 要选得可读、能清楚描述发生了什么,日后排查问题会轻松得多。
基于上面的清单,本应用使用的 8 个 action 为:
{type: 'todos/todoAdded', payload: todoText}{type: 'todos/todoToggled', payload: todoId}{type: 'todos/colorSelected', payload: {todoId, color}}{type: 'todos/todoDeleted', payload: todoId}{type: 'todos/allCompleted'}{type: 'todos/completedCleared'}{type: 'filters/statusFilterChanged', payload: filterValue}{type: 'filters/colorFilterChanged', payload: {color, changeType}}
这里大部分 action 只有一项额外数据,直接放进 action.payload 即可。颜色筛选本可以拆成"新增"和"移除"两个 action,但教程故意用一个带额外字段的 action 来表示,以演示 payload 也可以是对象。
和 state 数据一样,action 应只包含描述"发生了什么"所需的最小信息。
源码印证:action 的最低契约是
{ type: T extends string },见 src/types/actions.ts;store 的dispatch在运行时会强制校验"action 必须是纯对象"、"type不能为undefined"、"type必须是字符串",见 src/createStore.ts 中的dispatch实现——上面清单里的 8 个 action 全部满足这些约束。
编写 Reducers
Reducer 是接收当前 state 和 action 两个参数、返回新 state 的函数,即 (state, action) => newState。
对应到类型系统,src/types/reducers.ts 中的定义是:
export type Reducer<
S = any,
A extends Action = UnknownAction,
PreloadedState = S
> = (state: S | PreloadedState | undefined, action: A) => S
注意返回类型是 S 而不是 S | undefined——reducer 永远不允许返回 undefined,这一点在 combineReducers 的运行时校验中会被反复强调(见后文"源码剖析")。
创建根 Reducer
一个 Redux 应用实际上只有一个 reducer 函数:传入 createStore 的"根 reducer"(root reducer)。它负责处理所有被派发的 action,并计算每次的完整新 state。
在 src 文件夹(与 index.js、App.js 同层)创建 reducer.js。每个 reducer 都需要初始 state,所以先加入几条假数据,再写出 reducer 逻辑骨架:
const initialState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}
// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
reducer 可能在应用初始化时被以 undefined 作为 state 值调用;此时必须提供一个初始 state,后面的 reducer 代码才有东西可操作。Reducer 通常用默认参数语法提供初始 state:(state = initialState, action)。
处理 todos/todoAdded
先检查当前 action 的 type 是否匹配目标字符串,然后返回一个包含全部 state 字段的新对象——包括那些没有变化的字段:
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
加一个 Todo 要写这么多代码,为什么?这就引出了 reducer 必须遵守的规则。
Reducer 的三条铁律
Reducer 必须始终遵守若干特殊规则:
- 只能基于
state和action两个参数计算新 state 值; - 不允许修改现有的
state,必须做不可变更新(immutable updates)——复制现有state,再对复制出来的值做修改; - 不能执行任何异步逻辑或其他"副作用"。
"副作用"指任何在"从函数返回值之外"可观察到的状态或行为变化,常见的副作用包括:
- 向控制台打印日志;
- 保存文件;
- 设置异步定时器;
- 发起 HTTP 请求;
- 修改函数外部的某个状态,或原地修改(mutate)函数的参数;
- 生成随机数或唯一随机 ID(如
Math.random()、Date.now())。
凡是满足这些规则的函数都叫**"纯"函数**,即使它并不是以 reducer 的形式写出来的。
为什么这些规则如此重要?原因有几方面:
- Redux 的目标之一是可预测性:当函数的输出只由输入参数决定时,更容易理解它的行为,也更容易测试;
- 反之,如果函数依赖外部变量或行为随机,你就永远不知道运行结果会是什么;
- 如果函数修改了其他值(包括参数),会出乎意料地改变应用行为——这是常见 bug 来源,比如"我明明更新了 state,为什么 UI 该刷新时却不刷新";
- Redux DevTools 的部分能力(如时间旅行调试)依赖 reducer 正确遵守这些规则。
其中"不可变更新"这条规则尤其重要,值得单独展开。
Reducer 与不可变更新
Redux 中的两个基本概念是"mutation(变更/原地修改)"与"immutability(不可变性,把值视为不可更改)"。
在 Redux 中,reducer 绝对不允许修改原始/当前的 state 值!
// ❌ Illegal - by default, this will mutate the state! state.value = 123
为什么在 Redux 中不能 mutate state?
- 会引发 bug,例如 UI 无法正确更新到最新值;
- 更难理解 state 为什么、如何被更新;
- 更难编写测试;
- 破坏"时间旅行调试"的正确运行;
- 违背 Redux 的设计初衷与使用模式。
既然不能改原始值,返回更新后的 state 该怎么做?
Reducer 只能对原始值做"复制",然后才可以修改副本。
// ✅ This is safe, because we made a copy return { ...state, value: 123 }
Part 2 已经介绍过手动写不可变更新的方式:使用 JS 的数组/对象展开运算符,以及其他返回原值副本的函数(如 map、concat)。
当数据是嵌套的时这会变难。不可变更新的一条关键规则是:必须对每一个需要更新的嵌套层级都做一次复制。
如果你觉得"手写这种不可变更新既难记又容易错"——没错,官方也承认手写确实困难,而且 在 reducer 中不小心 mutate state 是 Redux 用户犯下的第一大错误。
好消息:在实际应用中,你不需要手写这些复杂的嵌套不可变更新。教程 Part 8(Modern Redux with Redux Toolkit,见 part-8)会讲如何用 Redux Toolkit(如
createSlice内部基于 Immer)简化 reducer 中的不可变更新写法。
继续处理更多 Actions
在掌握上述规则后,继续往根 reducer 中加逻辑。先按 ID 切换某个 Todo 的 completed 字段:
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
// Again copy the entire state object
...state,
// This time, we need to make a copy of the old todos array
todos: state.todos.map(todo => {
// If this isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}
// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}
再看一个筛选相关的 case,处理"可见性筛选值变化"(status 筛选)action:
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged': {
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}
此时只处理了 3 个 action,代码已经有些长了。如果继续把每个 action 都塞进这一个 reducer 函数,整体会越来越难读——这就是拆分 reducer 的动机。
Reducer 通常会被拆分为多个更小的 reducer 函数,以便理解和维护 reducer 逻辑。
拆分 Reducers:slice 文件与 feature 组织
Redux reducer 通常按"它更新哪一块 state"来拆分。本应用的 state 有两个顶层区域:state.todos 和 state.filters,因此可以把大的根 reducer 拆成两个小 reducer——todosReducer 和 filtersReducer。
官方建议按"feature"(与某个概念或业务区域相关的代码)组织 Redux 应用的文件夹与文件。某个 feature 的 Redux 代码通常写成单个文件,即"slice 文件",其中包含该部分 app state 的全部 reducer 逻辑与所有 action 相关代码。
因此,管理 state 某一节的 reducer 被称为 "slice reducer"。通常,部分 action 对象与某个 slice reducer 紧密相关,其 action type 字符串应以该 feature 名开头(如 'todos'),并描述发生的事件(如 'todoAdded'),拼接为一个字符串('todos/todoAdded')——这正是前文 8 个 action 命名的由来。
在工程中创建 features 文件夹,其下建 todos 文件夹,再创建 todosSlice.js,把 Todo 相关的初始状态剪切粘贴过来:
const initialState = [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
default:
return state
}
}
然后拷贝 Todo 的更新逻辑过来,但这里有一个重要区别:这个文件只负责更新 todos 相关的 state——它不再嵌套了! 这也是拆分 reducer 的另一个好处:由于 todos state 本身就是一个数组,slice 内部无需再复制外层的根 state 对象,reducer 因此更易读。
这种把多个 reducer 组合起来的方式叫 reducer composition(reducer 组合),是构建 Redux 应用的基本模式。
处理完两个 action 后,todosSlice.js 变为:
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Can return just the new todos array - no extra object around it
return [
...state,
{
id: nextTodoId(state),
text: action.payload,
completed: false
}
]
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}
代码变短了,也更好读了。
接下来对筛选逻辑做同样的事:创建 src/features/filters/filtersSlice.js,把筛选相关代码移过去:
const initialState = {
status: 'All',
colors: []
}
export default function filtersReducer(state = initialState, action) {
switch (action.type) {
case 'filters/statusFilterChanged': {
return {
// Again, one less level of nesting to copy
...state,
status: action.payload
}
}
default:
return state
}
}
这里仍然要复制包含 filters state 的对象,但由于嵌套层级少了一层,逻辑更直白。
官方教程为控制篇幅,跳过了其余 action(如
todos/colorSelected、todos/todoDeleted、todos/allCompleted、todos/completedCleared、filters/colorFilterChanged)的 reducer 更新逻辑展示,建议读者对照前文的需求清单自行练习;卡住时可以查阅教程末尾给出的 CodeSandbox 中的完整实现。
组合 Reducers:手写根 reducer 与 combineReducers
现在有了两个独立的 slice 文件、各自拥有一个 slice reducer。但前文说过,创建 store 时需要一个根 reducer。如何在不把代码写回一个巨型函数的前提下回到"单一根 reducer"?
由于 reducer 就是普通 JS 函数,可以把两个 slice reducer 导入 reducer.js,写一个只负责调用另外两个函数的新根 reducer:
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}
注意:每个 reducer 各自管理全局 state 的一块。每个 reducer 的 state 参数都不同,对应它自己所管理的那块 state。
这就让我们能够按 feature、按 state slice 拆分逻辑,保持可维护性。
combineReducers
观察上面手写的根 reducer:它对每个 slice 做的是同一件事——调用 slice reducer、传入该 reducer 所持有的 state 切片、把结果写回根 state 对象。如果再加 slice,这个模式会一直重复。
Redux 核心库自带一个工具函数 combineReducers,替我们完成这步样板代码。可以用它生成的更短的根 reducer 替换手写版本。
到这一步,真正需要安装 Redux 核心库了:
npm install redux
安装后导入并使用 combineReducers:
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})
export default rootReducer
combineReducers 接收一个对象:键名会成为根 state 对象的键名,值是知道如何更新对应 state 切片的 slice reducer 函数。
记住:你传给
combineReducers的键名,决定了 state 对象的键名!
当前仓库自带的示例 examples/todos-with-undo/src/reducers/index.js 就是这一模式的最小化实践:
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
export default todoApp
源码剖析:combineReducers 在运行时做了什么
文档说 combineReducers "替我们做了样板工作",但它实际还内置了一组帮助初学者避坑的校验逻辑。下面结合 src/combineReducers.ts 的实现逐条对照。
1. 过滤非函数值并告警
combineReducers 首先遍历传入对象的键,只有 typeof reducers[key] === 'function' 的值才会进入 finalReducers;在开发环境下(process.env.NODE_ENV !== 'production'),若某个键的值是 undefined,会输出 No reducer provided for key "xxx" 的 warning(见 src/combineReducers.ts)。对应的测试见 test/combineReducers.spec.ts:ignores all props which are not a function 与 warns if a reducer prop is undefined 两个用例分别验证了"非函数属性被忽略"和"undefined 触发警告"。
2. 两把"探针"检查 reducer 契约
assertReducerShape(见 src/combineReducers.ts)会对每个 slice reducer 做两次探针调用:
reducer(undefined, { type: '@@redux/INIT' }):如果返回undefined,抛出错误——"slice reducer 在初始化时返回了 undefined……当传入的 state 为 undefined 时,必须显式返回初始 state。初始 state 不能是 undefined"。这解释了教程中反复强调的 (state = initialState, action) 默认参数写法:store 初始化时就会以undefined调用每个 reducer;reducer(undefined, { type: 随机探针 type }):如果返回undefined,抛出错误——"不要用 slice reducer 处理'redux/*'命名空间下的@@redux/INIT等私有 action;对任何未知 action 你必须返回当前 state(若 state 为 undefined 则返回初始 state)"。
这两条校验对应了 docs/api/combineReducers.md "Notes" 一节列出的规则:未识别的 action 必须原样返回 state;永远不能返回 undefined;收到 undefined state 时必须返回该 reducer 的初始 state。测试 test/combineReducers.spec.ts 中的 throws an error if a reducer returns undefined handling an action 与 throws an error on first call if a reducer returns undefined initializing 用例验证了这两条路径。
3. 未变化的 slice 不重建根 state
组合 reducer 的核心循环(见 src/combineReducers.ts)对每个 key 依次执行 reducer(previousStateForKey, action),并用引用比较判断变化:
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
// ...
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
也就是说:只要某个 slice 对该 action"不感兴趣"而返回了原引用,且没有任何 slice 的引用发生变化、键数量也没变,组合 reducer 就直接返回旧的根 state 对象,而不是新建一个根对象。 这个"短路"行为有两个好处:一是为上层(如 React-Redux 的浅比较订阅)保留了"引用未变即无需重渲染"的优化空间;二是配合 dispatch 中 currentState = currentReducer(currentState, action) 的赋值(见 src/createStore.ts),让"无变化的 action 不产生新引用"成为可能。
另外,若某个 slice 对某个 action 返回了 undefined,这里会抛出带 action type 与 key 名的详细错误(the slice reducer for key "xxx" returned undefined...,见 src/combineReducers.ts),把"忘记 return state"这类错误尽早暴露。
4. 开发环境下的 state 形状检查
在开发环境,组合 reducer 还会调用 getUnexpectedStateShapeWarningMessage(见 src/combineReducers.ts)检查传入 state 的形状:如果 state 不是纯对象(借助 src/utils/isPlainObject.ts 判断),或含有任何不在 reducer 键列表中的"意外键",会输出警告(同一个意外键只警告一次,通过 unexpectedKeyCache 去重;@@redux/REPLACE action 则被豁免)。这对使用 preloadedState、服务端渲染水合等场景尤其有帮助。
5. 与 createStore 的联动:INIT 与 dispatch 校验
把视角拉回到 store。src/createStore.ts 在函数末尾执行 dispatch({ type: ActionTypes.INIT })(见 src/createStore.ts),注释明确说明:"当 store 被创建时,会派发一个 INIT action,让每个 reducer 返回其初始 state,从而填充初始 state 树。" 这就是教程中"reducer 可能以 undefined 被调用"的来源,也是 combineReducers 第一把探针(用 @@redux/INIT 探测)与之呼应的原因。
dispatch 本身则负责把"action 必须是纯对象且带字符串 type"这条契约落地(见 src/createStore.ts):
- 非纯对象(如 thunk 函数、Promise)会报错,提示可添加
redux-thunk等中间件; type为undefined或非字符串会报错;- 在 reducer 执行期间再调用
dispatch、getState、subscribe都会被禁止(Reducers may not dispatch actions.等错误),从机制上强化了"reducer 是纯函数"的规则。
类型层面,combineReducers 的重载(见 src/combineReducers.ts)会基于传入的 reducers 映射推断出组合 state 类型 StateFromReducersMapObject<M> 与 action 联合类型 ActionFromReducersMapObject<M>(定义于 src/types/reducers.ts),因此 combineReducers({ todos: todosReducer, filters: filtersReducer }) 的返回类型会自动携带 { todos: Todo[], filters: Filters } 这样的结构,preloadedState 也允许是 Partial 形状——这与"state 结构由键名决定"的运行时语义完全一致。
小结:State、Actions、Reducers 是 Redux 的基石
每个 Redux 应用都有 state 值、用于描述"发生了什么"的 action、以及基于先前 state 与 action 计算新 state 的 reducer 函数。官方教程对本部分要点的归纳如下:
- Redux 应用使用纯 JS 对象、数组与原始值作为 state 值
- 根 state 值应是一个纯 JS 对象;
- state 应只包含让应用工作所需的最小数据;
- 类、Promise、函数及其他非纯值不应该进入 Redux state;
- Reducer 中不允许创建
Math.random()、Date.now()这类随机值; - Redux store 之外完全可以并存其他 state(如组件局部 state)。
- Action 是带
type字段的纯对象,描述发生了什么type应是可读字符串,通常写作'feature/eventName'(如'todos/todoAdded');- Action 可携带其他值,通常放在
action.payload; - Action 应只携带描述"发生了什么"所需的最小数据。
- Reducer 形如
(state, action) => newState,必须始终遵守:- 只基于
state与action参数计算新 state; - 永远不 mutate 现有
state,始终返回副本; - 不做 HTTP 请求、异步逻辑等副作用。
- 只基于
- Reducer 应当拆分以便阅读
- 通常按顶层 state 键(即 state 的"切片")拆分;
- 通常写在 "slice" 文件中,按 "feature" 文件夹组织;
- 可以用 Redux 的
combineReducers组合; - 传给
combineReducers的键名决定了顶层 state 对象的键。
至此,我们已经拥有了一套能更新 state 的 reducer 逻辑,但这些 reducer 本身不会做任何事——它们需要放进一个 Redux store,由 store 在事件发生时带着 action 调用它们。下一篇 Part 4(Store,见 part-4)将讲解如何创建 Redux store 并让 reducer 逻辑真正跑起来。
延伸阅读(仓库内路径)
- src/combineReducers.ts:
combineReducers完整实现(含校验、短路逻辑); - src/createStore.ts:
createStore实现(INIT 派发、dispatch 校验、getState/subscribe/replaceReducer); - src/types/reducers.ts 与 src/types/actions.ts:
Reducer、ReducersMapObject、Action等类型定义; - docs/api/combineReducers.md:
combineReducersAPI 文档(规则、参数与示例); - test/combineReducers.spec.ts:上述运行时校验行为的测试用例;
- examples/todos-with-undo:
combineReducers组合todos+visibilityFilter两个 slice 的最小完整示例; - 教程上下文:Part 2: 概念与数据流、Part 4: Store、Part 8: Modern Redux with Redux Toolkit。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
