首页
/ Redux 核心概念全解:State、Action 与 Reducer 如何在源码中协作

Redux 核心概念全解:State、Action 与 Reducer 如何在源码中协作

2026-09-04 14:39:29作者:邵娇湘

本篇基于 Redux 仓库中的 CoreConcepts.md 文档,完整还原其“纯对象状态 + Action + Reducer”的核心思想,并深入 src 源码目录印证这些概念在 createStorecombineReducers 中的真实实现机制。读完本文,你将既能从零理解 Redux 的设计动机,也能对照源码说清楚 dispatch 之后每一步发生了什么。

Redux 单向数据流示意图

一、核心思想: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.tsgetStateInMiddle 系列断言专门验证了这条规则。

二、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 就像一串“发生了什么”的面包屑。

源码为这条约定设置了明确的运行时边界:

  1. 类型定义src/types/actions.ts 将 Action 定义为一个带 type 字段的普通对象,并且注释强调“类型必须是字符串,因为字符串可以序列化”;UnknownAction / AnyAction 则允许 action 携带任意额外属性。

  2. dispatch 时的三重校验src/createStore.ts 中,dispatch 会依次检查:

    • action 必须是 plain object,否则抛出 Actions must be plain objects...,并提示你可能需要 redux-thunk 之类中间件来处理函数类型的 dispatch 值;
    • action.type 不能是 undefined,防止 action type 字符串常量拼写错误;
    • action.type 必须是字符串。
  3. 工具函数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.tsReducer 类型注释明确写道:reducer 必须是纯函数、不能有副作用,“这正是热重载和时间旅行这类特性的基础”,并且特别提醒“不要把 API 调用放进 reducer”。
  • 不认识的动作必须原样返回 statedefault: return state 不是可有可无的写法。src/utils/actionTypes.ts 的注释规定:对于任何未知 action,必须返回当前 state;如果当前 state 是 undefined,必须返回初始 state。
  • 用参数默认值表达初始 statestate = []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 调用”多做的工作:

  1. 构建期断言assertReducerShapesrc/combineReducers.ts)会用 reducer(undefined, { type: INIT }) 探测每个子 reducer,若返回 undefined 立即抛出“slice reducer for key ... returned undefined during initialization”错误;再用一个随机 PROBE_UNKNOWN_ACTION 探测它对未知动作的行为。这两类错误都是开发者最常踩的坑(忘记处理初始 state、忘记 default: return state),combineReducers 把它们提前到了组合时暴露。
  2. 运行期兜底:组合出的 combination 函数在每次调用时,若某个子 reducer 返回 undefined,会抛出包含 action type 的明确错误;同时用 nextStateForKey !== previousStateForKey 逐 key 判断是否有变化,没有任何变化时直接返回原 state 引用hasChanged ? nextState : state),这为依赖引用比较的订阅/渲染层省下了无谓的重计算。
  3. 开发期警告:非生产环境下,它还会检查传入 state 中是否出现了 reducer key 之外的“意外键”(如 server 渲染水合时字段拼错),并提示这些键将被忽略。

combineReducerscreateStore 的衔接由类型系统保证:src/index.ts 同时导出 createStorecombineReducersbindActionCreatorsapplyMiddlewarecomposeisActionisPlainObject 以及以 __DO_NOT_USE__ActionTypes 命名导出的私有 action type——下划线前缀的名字就是在提醒使用者:@@redux/INIT@@redux/REPLACE 这类保留 type 是内部机制,不应在业务代码中直接引用。

五、Store:概念闭环的最后一块拼图

原文档最后强调“这基本上就是 Redux 的全部想法”,并指出上面整个流程没有用到任何 Redux API。但如果要真正跑起来,还需要一个持有 state、串联 dispatch 与订阅者的容器,这就是 store。从 src/createStore.ts 的实现可以完整走一遍这个闭环:

  1. store 创建即触发 INITcreateStore 函数体末尾执行 dispatch({ type: ActionTypes.INIT })src/createStore.ts),让每个 reducer 返回初始 state,从而“填充”初始状态树——这就是为什么 reducer 参数默认值如此重要。
  2. dispatch 是唯一的写入口:校验通过后进入 try { isDispatching = true; currentState = currentReducer(currentState, action) } finally { isDispatching = false },即“新 state = reducer(旧 state, action)”,然后通知所有监听器。测试 test/createStore.spec.tsapplies the reducer to the previous state 用例逐次 dispatch ADD_TODO 并断言 state 数组按预期增长,正是对这一机制的验证。
  3. 订阅与快照subscribe 返回 unsubscribe 函数;内部用 currentListeners / nextListeners 双 Map 做浅拷贝(ensureCanMutateNextListeners),保证在 dispatch 进行中的订阅/退订不会影响当前这一轮通知,避免竞态 bug。
  4. 防重入isDispatching 标记让 reducer 中再次 dispatchgetStatesubscribe 都会抛出明确错误(“Reducers may not dispatch actions”),从机制上杜绝了递归更新。
  5. replaceReducer:用于代码分割时动态加载 reducer 或热更新,它替换 currentReducer 后 dispatch 一个 REPLACE action,使新旧 reducer 的交集保留原有 state。

测试还验证了 store 的公共 API 面就是四样东西:subscribedispatchgetStatereplaceReducer(见 test/createStore.spec.tsexposes 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/toolkitconfigureStore(核心包更适合用于学习目的),若确实要用核心包而又不想看到弃用警告,可改用 legacy_createStore 导入(见 src/createStore.tssrc/index.ts)。

如果你想动手验证本文的所有结论,仓库提供了两条路径:

  • 直接运行示例:examples/todos 是一个完整的小型 todo 应用,其 actionsreducers 与本文第二节、第三节的 action/reducer 模式一一对应;
  • 查看实现与测试:test/createStore.spec.tstest/combineReducers.spec.ts 用 vitest 覆盖了 dispatch 校验、初始 state、订阅快照等全部关键行为,运行 yarn test(即 vitest --run --typecheck)即可复现。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384