Redux Store 核心 API 详解:getState、dispatch 与 subscribe 的用法及源码实现原理
Store 是 Redux 中持有整个应用状态树的唯一对象,也是 Redux 数据流的枢纽。本篇以 Redux 官方 API 文档中的 Store 页面 为主体,逐一讲清 Store 的四个公开方法 getState()、dispatch(action)、subscribe(listener)、replaceReducer(nextReducer) 的参数、返回值、使用限制与注意事项,并结合 createStore 实现源码、Store 类型定义 与 createStore 测试用例 展开源码级佐证,帮助你在理解 API 约定的同时,看懂这些约定在实现中是如何被强制执行的。
Store 是什么:不是类,而是一个带方法的普通对象
一个 Store 持有应用的整棵状态树,改变其中状态的唯一方式是向其 dispatch 一个 action,这会触发根 reducer 计算出新状态。官方文档明确强调:Store 不是一个类,它只是一个带有几个方法的对象(a plain object with a few methods)。
这个"纯对象"定位决定了它的可增强性:正因为 Store 不是实例,它的拷贝可以被轻松创建和修改而无需改变原对象,这也是 Store Enhancer(如中间件)能够以高阶函数方式组合出增强版 Store 的前提。在 Store 类型定义 中可以看到,Store 接口只有五个成员:
dispatch:分发 action 的唯一入口getState():读取当前状态树subscribe(listener):注册变化监听器,返回取消订阅函数replaceReducer(nextReducer):动态替换当前 reducer[Symbol.observable]():为可观察/响应式库提供的互操作点
仓库内的 counter-vanilla 示例 展示了 Store 最基础的使用形态:创建 store、render() 读取 store.getState() 更新 DOM、store.subscribe(render) 注册监听、按钮事件里 store.dispatch({ type: 'INCREMENT' }) 触发状态变更。
创建 Store 时,官方当前推荐的做法是:把根 reducer 传给 Redux Toolkit 的 configureStore 方法;如果尚未使用 Redux Toolkit,则可以使用原始的 createStore 方法。需要注意,自 Redux 4.2.0 起,核心包的 createStore 已被标记为 @deprecated(仅是编辑器中的视觉删除线提示,无运行时错误,且永不移除);从当前仓库 源码 的 JSDoc 可以看到,若想避免这个删除线提示,可以改用同名的替代导出 legacy_createStore:
import { legacy_createStore as createStore } from 'redux'
createStore 文档 也给出了三个处理建议:迁移到 Redux Toolkit 的 configureStore、保持现状忽略提示、或改用 legacy_createStore 别名导入。
getState():读取当前状态树
getState() 返回应用的当前状态树,其值等于 store 的 reducer 最后一次返回的结果。
// 返回值:
// (any): 应用的当前状态树
function getState(): S
从 实现源码 看,这个方法有一处重要的运行时约束:
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 执行期间(isDispatching 为 true)调用 getState() 会直接抛错——因为 reducer 已经以参数形式收到了当前状态,此时再读 store 属于不纯行为,应该把状态从顶层 reducer 一路传下去。测试文件 createStore.spec.ts 中的 does not allow getState() from within a reducer 用例专门验证了这一行为:在 reducer 内部调用 store.getState 会匹配到 /You may not call store.getState()/ 错误。
另一个常见用途是在 subscribe 的回调里调用 getState() 读取最新状态——因为监听器本身不接收任何参数(见下文 subscribe 一节),读取状态必须由监听器主动完成。
dispatch(action):触发状态变更的唯一方式
dispatch 是 Store 上唯一能触发状态变更的方法。被 dispatch 的 action 会与 getState() 的当前结果一起同步传给 store 的 reducer,reducer 的返回值被视为下一个状态,从此 getState() 将返回它,且所有 change listener 会立即被通知。
参数:action 必须是带 type 的普通对象
action(Object):一个描述"发生了什么变化"的普通对象。Actions 是数据进入 store 的唯一途径,无论是 UI 事件、网络回调还是 WebSocket 等来源的数据,最终都要以 action 的形式被 dispatch。action 必须有一个type字段来标识动作类型,type通常定义为常量并从其他模块导入;使用字符串而非 Symbol 更好,因为字符串是可序列化的。除type之外,action 的其余结构完全由你决定。
关于 "vanilla" store 与中间件的区分,文档中有两点关键说明:
- 通过
createStore得到的基础 store 只支持普通对象 action,并立即把它交给 reducer; - 如果用
applyMiddleware包装createStore,中间件可以以不同方式解释 action,从而支持 dispatch 异步 action(如 Promise、Observable、thunk 等异步原语)。中间件由社区提供,Redux 本身不默认携带,需要显式安装如redux-thunk、redux-promise等包,也可以自行编写。
Dispatch 类型定义 的注释对此总结得很精确:基础 dispatch 函数总是同步地把 action 连同前一个状态一起发给 reducer;中间件则包装这个基础 dispatch,在把 action 传给下一个中间件之前可以对其进行转换、延迟、忽略或重新解释。
返回值
返回被 dispatch 的那个 action 对象本身,方便链式写法。注意:如果使用了自定义中间件,它可能包装 dispatch() 而返回别的东西(例如一个可 await 的 Promise)——类型定义注释 专门强调了这一点。
示例
import { createStore } from 'redux'
const store = createStore(todos, ['Use Redux'])
function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))
源码中的四道校验关卡
dispatch 的实现 在真正调用 reducer 之前依次做了四道校验,每一道都对应一条会抛出的具体错误:
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(action)}'. ...`
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
)
}
if (typeof action.type !== 'string') {
throw new Error(`Action "type" property must be a string. ...`)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => {
listener()
})
return action
}
对应的 测试用例 覆盖了每一种失败形态:dispatch 非对象值(null、字符串、实例对象等)报 plain 错误;缺少 type 报 undefined "type" property;type 为布尔、数字、Symbol 时报 "type must be a string"(而空字符串 '' 合法)。
注意最后一段:isDispatching 标志位在 try/finally 中管理,即使 reducer 抛错,标志位也会复位——测试用例 recovers from an error within a reducer(createStore.spec.ts)验证了 reducer 抛错后 store 仍可用。而 isDispatching 同时就是文档中 "Reducers may not dispatch actions" 警告的底层机制:reducer 执行期间任何 dispatch 调用都会命中第四道校验而抛错。
在 reducer 之外 dispatch 的正确位置
文档特别提醒:如果在 reducer 内部尝试 dispatch,会抛出 "Reducers may not dispatch actions." 错误——reducer 是纯函数,只能返回新状态,不能有副作用(dispatch 就是副作用)。而在 Redux 中,订阅监听器是在根 reducer 返回新状态之后才被调用的,因此在 subscribe 的监听器里 dispatch 是允许的;禁止 dispatch 的只有 reducer 内部。如果想在响应某个 action 时执行副作用,正确的位置是(可能异步的)action creator。
subscribe(listener):注册变化监听器
subscribe 用于添加变化监听器:每次有 action 被 dispatch、状态树的某部分可能已改变时,它都会被调用。在回调内你可以调用 getState() 读取当前状态树。
在监听器中 dispatch 的三条注意事项
文档明确允许在 change listener 中调用 dispatch(),但附带三条约定:
- 要有条件地 dispatch。监听器应只在响应用户动作或满足特定条件(例如 store 中某字段达到某值)时才 dispatch。无条件 dispatch 技术上可行,但会导致无限循环——因为每次
dispatch()通常又会触发监听器。 - 订阅列表在每次 dispatch 前被快照。如果监听器正在被调用的过程中发生订阅或退订,不会影响当前正在进行的这次
dispatch();但下一次dispatch()(无论是否嵌套)会使用更新的订阅列表快照。 - 不要期待看到每一次状态变化。在一次嵌套
dispatch()过程中状态可能被更新了多次,监听器被调用时不一定能看到中间状态;但保证是:在dispatch()退出时,所有在其开始前注册的订阅者都会以最新状态被调用。
这三条并非纸面约定,createStore 测试文件 中有成体系的用例逐条验证:supports removing a subscription within a subscription(监听器内部退订自己只影响后续调用)、notifies all subscribers about current dispatch regardless if any of them gets unsubscribed in the process(本次 dispatch 内退订不影响本次其余监听器被通知)、notifies only subscribers active at the moment of current dispatch(dispatch 期间新加入的订阅者本次不被通知,下次才生效)、uses the last snapshot of subscribers during nested dispatch(嵌套 dispatch 使用最新的快照)。
参数与返回值
listener(Function):每次 action 被 dispatch 且状态树可能改变时被调用的回调。可以在回调内调用getState()读取当前状态树。由于可以合理期望 reducer 是纯函数,你可以通过比较状态树中某个深层路径的引用来判断其值是否变化。- 返回值:一个用于退订该监听器的函数。调用
subscribe返回的函数即可取消订阅;实现中 用isSubscribed标志保证退订函数是幂等的(重复调用安全),且同一监听函数订阅多次时,每个退订函数只移除对应的那一个订阅(测试用例only removes relevant listener when unsubscribe is called验证了这一点)。
另外从源码可以看到两个与 reducer 执行期的互斥约束:subscribe 和退订函数在 reducer 执行期间调用都会抛错(createStore.ts 的 isDispatching 检查),对应测试用例 does not allow subscribe() from within a reducer。
示例:比较深层引用判断变化
function select(state) {
return state.some.deep.property
}
let currentValue
function handleChange() {
let previousValue = currentValue
currentValue = select(store.getState())
if (previousValue !== currentValue) {
console.log(
'Some deep nested property changed from',
previousValue,
'to',
currentValue
)
}
}
const unsubscribe = store.subscribe(handleChange)
unsubscribe()
这是 vanilla Redux 中"选择器 + 引用比较"的经典手动模式:利用 reducer 的不可变性,只有被真正改变的分支引用才会变化,从而避免对无关的 action 做出反应。
定位:底层 API
文档指出 subscribe 是一个底层 API——大多数场景你更可能通过 React 等绑定库来使用 store,而不是直接调用它。如果经常用回调作为响应状态变化的钩子,可以考虑编写一个自定义的 observeStore 工具;而且 Store 本身还是一个 Observable,可以用 RxJS 等库直接 subscribe 其状态变化。这一点由 实现中的 observable() 方法 支撑:它返回的对象首次 subscribe 时会立即发出一次当前状态(observeState()),之后每次 dispatch 后再次发出,测试文件中的 RxJS 集成测试(should pass an integration test with a common library (RxJS))验证了 from(store) 可以正常消费状态流。
replaceReducer(nextReducer):动态替换 reducer
replaceReducer 用于替换 store 当前用来计算状态的 reducer。这是一个高级 API,典型场景有两个:应用实现了代码分割、希望动态加载部分 reducer;或者实现 Redux 的热重载机制。
参数
nextReducer(Function):store 将要使用的下一个 reducer。
实现细节:替换后立即用一个内部 action 重算状态
从 实现源码 可以看到,replaceReducer 做两件事:
function replaceReducer(nextReducer: Reducer<S, A>): void {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(nextReducer)}'`
)
}
currentReducer = nextReducer as unknown as Reducer<S, A, PreloadedState>
// This action has a similar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE } as A)
}
替换 currentReducer 后,它会 dispatch 一个 @@redux/REPLACE 类型的内部 action,效果类似 store 创建时的 @@redux/INIT:新旧 rootReducer 中同名路径上都存在的子 reducer 会收到旧状态,从而把旧状态树中的相关数据填充进新状态树——这正是"替换 reducer 后状态得以保留"的机制。这些私有 action type 定义在 actionTypes.ts 中,且带有随机后缀(防止与用户 action 冲突),文档注释明确提醒不要在自己的代码中直接引用它们。
测试用例 preserves the state when replacing a reducer 完整演示了这一行为:先 dispatch 两个 addTodo,然后 replaceReducer(todosReverse) 后状态原样保留,新 reducer 继续在其上工作;再换回原 reducer 状态依然连续。传非函数则抛出 Expected the nextReducer to be a function(测试)。
创建时的一次隐式 dispatch:INIT
理解 Store 完整行为还差最后一环:createStore 函数末尾 在把 store 对象返回之前,会先 dispatch 一个 @@redux/INIT 内部 action,让每个 reducer 返回其初始状态,从而填充初始状态树。这意味着:
- 你的 reducer 应当处理"第一个参数为
undefined"的情况并返回初始状态; - 你并不打算、也不应该直接处理这个 dummy action;
- 使用
preloadedState(第二个参数)时,INIT 会以该状态为起点调用 reducer,因此传入的初始状态会经过一次 reducer 计算——createStore 文档的 Tips 与测试用例passes the initial state/applies the reducer to the initial state都覆盖了这一点。
这也解释了为什么 counter-vanilla 示例 中 reducer 要写 if (typeof state === 'undefined') return 0 这一分支。
Store 方法的调用时序总览
结合 dispatch 实现 与 isDispatching 标志位,一次 dispatch 的完整时序是:
- 校验 action(plain object、
type存在且为字符串); - 置
isDispatching = true(此期间禁止dispatch/getState/subscribe/ 退订,违者抛错); currentState = currentReducer(currentState, action)(reducer 抛错时经finally复位标志位,store 保持可用);- 将
nextListeners固化为currentListeners(订阅快照机制:dispatch 期间发生的订阅/退订写入nextListeners,不影响本次遍历); - 逐个同步调用监听器,监听器内可安全地嵌套 dispatch(测试用例
handles nested dispatches gracefully验证); - 返回 action 本身。
小结与参考
| 方法 | 作用 | 关键约束(源自码与测试) |
|---|---|---|
getState() |
读取当前状态树 | reducer 执行期间调用会抛错 |
dispatch(action) |
唯一的状态变更入口 | 只接受带字符串 type 的 plain object;reducer 内调用会抛错 |
subscribe(listener) |
注册变化监听器 | 返回幂等的退订函数;订阅列表按每次 dispatch 前快照生效 |
replaceReducer(nextReducer) |
动态替换 reducer | 替换后 dispatch 内部 REPLACE action,同名分支保留旧状态 |
如果你想继续深入当前仓库,建议按以下顺序阅读:Store 类型定义(接口与 JSDoc 的完整约定)、createStore 实现(上述所有行为的落地代码)、createStore 测试(每一条约定的可执行验证)、applyMiddleware 文档(dispatch 如何被中间件增强)、以及 createStore 文档(preloadedState、enhancer 参数与 legacy_createStore 的弃用说明)。以上结论均基于当前仓库(redux 5.0.1)的实际源码与测试;如果你使用 Redux Toolkit,对应的 store 创建入口是其 configureStore,但本文讲解的 Store 方法语义完全一致。
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