Redux 核心概念全解:State、Action 与 Reducer 如何在源码中协作
本篇基于 Redux 仓库中的 CoreConcepts.md 文档,完整还原其“纯对象状态 + Action + Reducer”的核心思想,并深入 src 源码目录印证这些概念在 createStore、combineReducers 中的真实实现机制。读完本文,你将既能从零理解 Redux 的设计动机,也能对照源码说清楚 dispatch 之后每一步发生了什么。
一、核心思想:State 是一个纯 JavaScript 对象
Redux 的出发点非常简单:想象你的应用状态被描述为一个纯对象。以一个 todo 应用为例,它的状态可能是这样:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
这个对象类似于一个“model”,但没有 setter——这正是刻意的设计:不让应用的不同部分随意修改状态,否则会产生难以复现的 bug。状态只能通过一种受控途径变化。
从源码层面看,这个“状态树”由 store 内部的一个变量持有。src/createStore.ts 中,createStore 创建 store 时声明了 currentState 闭包变量,它只能被 dispatch 内部这一处更新;对外读取状态的唯一入口是 getState():
// src/createStore.ts
function getState(): S {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState as S
}
注意一个容易忽视的细节:reducer 执行期间禁止调用 getState()(见 src/createStore.ts)。因为 reducer 已经通过参数收到了当前状态,再回头去 store 里读状态会破坏“reducer 是纯函数”的承诺。测试用例 test/createStore.spec.ts 中 getStateInMiddle 系列断言专门验证了这条规则。
二、Action:唯一改变状态的方式,且必须是一个普通对象
要改变状态中的某一部分,需要 dispatch 一个 action。Action 是一个普通的 JavaScript 对象(注意这里没有任何魔法),描述“发生了什么”。原文档给出的几个典型 action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
强制“每次变化都必须描述为一个 action”的价值在于:状态变了,我们一定知道它为什么变。Action 就像一串“发生了什么”的面包屑。
源码为这条约定设置了明确的运行时边界:
-
类型定义:src/types/actions.ts 将 Action 定义为一个带
type字段的普通对象,并且注释强调“类型必须是字符串,因为字符串可以序列化”;UnknownAction/AnyAction则允许 action 携带任意额外属性。 -
dispatch 时的三重校验:src/createStore.ts 中,
dispatch会依次检查:- action 必须是 plain object,否则抛出
Actions must be plain objects...,并提示你可能需要redux-thunk之类中间件来处理函数类型的 dispatch 值; action.type不能是undefined,防止 action type 字符串常量拼写错误;action.type必须是字符串。
- action 必须是 plain object,否则抛出
-
工具函数:src/utils/isAction.ts 导出
isAction,判断逻辑就是“plain object + 存在 string 类型的type”,与上面 dispatch 的校验规则保持一致,可在业务代码中复用来防御非法 action。
三、Reducer:接收 state 和 action,返回下一个 state
把 state 和 action 连接起来的,是一个叫 reducer 的函数。同样没有魔法——它就是接收 state 和 action 两个参数、返回应用下一个状态的普通函数。原文档给出了两个管理“状态的一部分”的小 reducer:
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
这段代码体现了 reducer 的三条硬性契约,均可在源码与类型定义中找到对应物:
- 纯函数:src/types/reducers.ts 的
Reducer类型注释明确写道:reducer 必须是纯函数、不能有副作用,“这正是热重载和时间旅行这类特性的基础”,并且特别提醒“不要把 API 调用放进 reducer”。 - 不认识的动作必须原样返回 state:
default: return state不是可有可无的写法。src/utils/actionTypes.ts 的注释规定:对于任何未知 action,必须返回当前 state;如果当前 state 是undefined,必须返回初始 state。 - 用参数默认值表达初始 state:
state = []、state = 'SHOW_ALL'让 reducer 在 store 初始化(state 为undefined)时也能给出合法初始值。仓库内的真实示例 examples/todos/src/reducers/todos.js 沿用了完全相同的模式:const todos = (state = [], action) => {...},并在ADD_TODO/TOGGLE_TODO分支里通过展开运算符做不可变更新。
四、组合 Reducer:从“状态的一部分”到完整状态树
写一个大应用的单一 reducer 会很痛苦,所以 Redux 的实践是:写多个小 reducer 各自管理状态的一部分,再写一个 reducer 管理完整状态。原文档的示例是调用两个子 reducer 并组装结果:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}
这个手写版本对应库中内置的 combineReducers 工具。src/combineReducers.ts 揭示了它比“按 key 调用”多做的工作:
- 构建期断言:
assertReducerShape(src/combineReducers.ts)会用reducer(undefined, { type: INIT })探测每个子 reducer,若返回undefined立即抛出“slice reducer for key ... returned undefined during initialization”错误;再用一个随机PROBE_UNKNOWN_ACTION探测它对未知动作的行为。这两类错误都是开发者最常踩的坑(忘记处理初始 state、忘记default: return state),combineReducers把它们提前到了组合时暴露。 - 运行期兜底:组合出的
combination函数在每次调用时,若某个子 reducer 返回undefined,会抛出包含 action type 的明确错误;同时用nextStateForKey !== previousStateForKey逐 key 判断是否有变化,没有任何变化时直接返回原 state 引用(hasChanged ? nextState : state),这为依赖引用比较的订阅/渲染层省下了无谓的重计算。 - 开发期警告:非生产环境下,它还会检查传入 state 中是否出现了 reducer key 之外的“意外键”(如 server 渲染水合时字段拼错),并提示这些键将被忽略。
combineReducers 与 createStore 的衔接由类型系统保证:src/index.ts 同时导出 createStore、combineReducers、bindActionCreators、applyMiddleware、compose、isAction、isPlainObject 以及以 __DO_NOT_USE__ActionTypes 命名导出的私有 action type——下划线前缀的名字就是在提醒使用者:@@redux/INIT、@@redux/REPLACE 这类保留 type 是内部机制,不应在业务代码中直接引用。
五、Store:概念闭环的最后一块拼图
原文档最后强调“这基本上就是 Redux 的全部想法”,并指出上面整个流程没有用到任何 Redux API。但如果要真正跑起来,还需要一个持有 state、串联 dispatch 与订阅者的容器,这就是 store。从 src/createStore.ts 的实现可以完整走一遍这个闭环:
- store 创建即触发 INIT:
createStore函数体末尾执行dispatch({ type: ActionTypes.INIT })(src/createStore.ts),让每个 reducer 返回初始 state,从而“填充”初始状态树——这就是为什么 reducer 参数默认值如此重要。 - dispatch 是唯一的写入口:校验通过后进入
try { isDispatching = true; currentState = currentReducer(currentState, action) } finally { isDispatching = false },即“新 state = reducer(旧 state, action)”,然后通知所有监听器。测试 test/createStore.spec.ts 的applies the reducer to the previous state用例逐次 dispatchADD_TODO并断言 state 数组按预期增长,正是对这一机制的验证。 - 订阅与快照:
subscribe返回 unsubscribe 函数;内部用currentListeners/nextListeners双 Map 做浅拷贝(ensureCanMutateNextListeners),保证在 dispatch 进行中的订阅/退订不会影响当前这一轮通知,避免竞态 bug。 - 防重入:
isDispatching标记让 reducer 中再次dispatch、getState或subscribe都会抛出明确错误(“Reducers may not dispatch actions”),从机制上杜绝了递归更新。 replaceReducer:用于代码分割时动态加载 reducer 或热更新,它替换currentReducer后 dispatch 一个REPLACEaction,使新旧 reducer 的交集保留原有 state。
测试还验证了 store 的公共 API 面就是四样东西:subscribe、dispatch、getState、replaceReducer(见 test/createStore.spec.ts 的 exposes the public API),与原文档“描述状态如何随 action 演进”的最小闭环完全吻合。
六、结论:90% 的代码是纯 JavaScript
回到原文档最核心的判断:Redux 附带了一些工具函数(combineReducers 等)来方便这种模式,但主体思想是描述状态如何随时间、对 action 对象做出响应地更新;你写的 90% 代码只是普通 JavaScript,不用 Redux 本身、不用它的任何 API、也没有魔法。
这一点在当前仓库的 package.json 中也有呼应:包描述为 "A JS library for predictable and maintainable global state management"(当前版本 5.0.1)。同时源码注释给出的实践建议值得注意:createStore 已被标记为 deprecated,官方推荐生产代码使用 @reduxjs/toolkit 的 configureStore(核心包更适合用于学习目的),若确实要用核心包而又不想看到弃用警告,可改用 legacy_createStore 导入(见 src/createStore.ts 与 src/index.ts)。
如果你想动手验证本文的所有结论,仓库提供了两条路径:
- 直接运行示例:examples/todos 是一个完整的小型 todo 应用,其 actions、reducers 与本文第二节、第三节的 action/reducer 模式一一对应;
- 查看实现与测试:test/createStore.spec.ts、test/combineReducers.spec.ts 用 vitest 覆盖了 dispatch 校验、初始 state、订阅快照等全部关键行为,运行
yarn test(即vitest --run --typecheck)即可复现。
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 StartedRust0622
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
