Redux 仓库 Store Setup 深度指南:单 Store 架构、中间件链与订阅机制的设计取舍
本篇基于 Redux 仓库的 FAQ 文档 Store Setup 与核心源码,系统解答 Store 配置层面的三个高频问题:为什么应用应当只使用单一 Store、中间件链中 next 与 dispatch 的行为差异、以及如何订阅状态变化并获取其背后的设计动机。读完本文,你将能够正确配置 Store 与中间件、理解 Redux 底层订阅快照机制,并知道在多 Store、订阅裁剪、服务端渲染等场景下该如何做技术选型。
一、能否创建多个 Store?能否直接 import Store 使用?
1.1 单 Store 是 Redux 的既定模式
最初的 Flux 架构提倡一个应用中存在多个 "store",每个 store 持有一块领域数据。这种模式会引入诸如一个 store 需要 waitFor 另一个 store 完成更新才能同步的问题。Redux 不需要这样做,因为领域数据的拆分已经通过把一个根 reducer 拆分为多个小 reducer 并组合(combineReducers)这一机制天然实现了。
FAQ 原文明确指出:在一个页面中创建多个相互独立的 Redux store 是可能的,但预期的模式是只有一个 store。单一 store 带来的直接收益包括:
- 可以完整使用 Redux DevTools 进行时间旅行调试;
- 数据持久化(persist)与恢复(rehydrate)的逻辑更简单;
- 订阅(subscription)逻辑更简单。
1.2 多 Store 的两个合理例外
FAQ 也列举了少数正当使用多个 store 的场景:
- 性能问题:某部分状态更新过于频繁,且已经通过 profiling 确认是瓶颈时,可以将其拆到独立 store;
- 应用隔离:把一个 Redux 应用作为组件嵌入更大的应用时,可以为每个根组件实例创建一个 store。
但原文强调:创建新 store 不应该是第一反应(尤其是来自 Flux 背景的开发者的第一反应)。应优先尝试 reducer 组合,只有在它解决不了问题时才考虑多 store。这一立场也可以从 createStore 的 JSDoc 中得到印证——src/createStore.ts 中官方文档注释写道:
/**
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several reducers
* into a single reducer function by using `combineReducers`.
*/
即“你的应用应当只有一个 store;要让状态树的不同部分响应不同的 action,请使用 combineReducers 组合多个 reducer”。
1.3 直接 import 导出 Store 会带来什么问题?
FAQ 指出:虽然你可以通过直接 import store 实例来引用它,但这不是推荐模式。如果把 store 实例创建后从模块中导出,它就变成了一个单例(singleton),这会造成两个后续难题:
- 以后如果需要把一个 Redux 应用隔离为更大应用的组件,单例会成为障碍;
- 启用服务端渲染时同样受阻——因为在服务器上,每个请求都需要创建独立的 store 实例,避免请求之间共享可变状态。
仓库中的 服务端渲染文档 给出了这一要求的完整操作:服务端需要“为每个请求创建一个全新的 Redux store 实例 → 可选地 dispatch 一些 action → 从 store 中取出 state → 随响应把 state 传给客户端”;客户端再用服务端传来的 state 初始化自己的 store。仓库中的 universal 示例(同目录下 common/store/configureStore.js 按环境拆分出 dev/prod 两个配置文件)正是这一“每请求建 store”模式的落地代码,可对照阅读。
1.4 推荐做法:用 Provider 向下传递 Store
在使用 React Redux 时,connect() 生成的包装组件确实会在 props.store 存在时优先使用它,但最佳实践是用 <Provider store={store}> 包裹根组件,让 React Redux 负责向下传递 store。这样做的好处是:组件不需要关心 import 哪个 store 模块,将来隔离应用或启用服务端渲染都会容易得多。
一个最小可用的 store 创建形态可以参考仓库中的 counter-vanilla 示例——它不依赖任何构建工具或视图框架,直接用原生 DOM 演示了最纯粹的 Redux API:
var store = Redux.createStore(counter)
function render() {
valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)
document
.getElementById('increment')
.addEventListener('click', function () {
store.dispatch({ type: 'INCREMENT' })
})
注意其中只创建了一个 createStore(counter) 实例,UI 更新完全通过 subscribe(render) 驱动,这正是单 store + 订阅模式的原始形态。
二、Store Enhancer 中能否有多条中间件链?next 与 dispatch 有何区别?
2.1 中间件行为如链表
Redux 的中间件行为像一条链表(linked list)。每个中间件函数有三种选择:
- 调用
next(action):把 action 传给链中的下一个中间件; - 调用
dispatch(action):从头开始重新进入整条链; - 什么都不做(例如 return 而不转发):阻止 action 被进一步处理。
这条中间件链由创建 store 时传给 applyMiddleware 的参数定义。FAQ 明确指出:定义多条独立的中间件链不能正确工作,因为不同的链拥有各自不同的 dispatch 引用,多条链之间实际上是互相断开的。
2.2 源码印证:链是如何被组装的
查看 src/applyMiddleware.ts 的实现,可以看到链的组装过程:
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
这里有几个关键细节可以直接回答“为什么不能有多条链”:
- 一条链 = 一次
compose闭包。applyMiddleware内部通过compose(...chain)(store.dispatch)把全部中间件串成一个整体,并返回一个替换了dispatch的新 store 对象。如果你在应用里分别调用两次applyMiddleware(...)生成两个 store,每个 store 的dispatch闭包都只覆盖各自的中间件,链与链之间没有任何引用关系——这正是 FAQ 所说“不同的dispatch引用导致链被断开”的源码级原因。 - 构造期 dispatch 被显式禁止。注意初始的
dispatch是一个会抛错的占位函数:在中间件初始化阶段(applyMiddleware执行过程中)调用store.dispatch会直接抛出Dispatching while constructing your middleware is not allowed。这一点有对应的测试用例验证,见 test/applyMiddleware.spec.ts 中的'warns when dispatching during middleware setup':
it('warns when dispatching during middleware setup', () => {
function dispatchingMiddleware(store: Store) {
store.dispatch(addTodo("Don't dispatch in middleware setup"))
return (next: Dispatch) => (action: Action) => next(action)
}
expect(() =>
applyMiddleware(dispatchingMiddleware as Middleware)(createStore)(
reducers.todos
)
).toThrow()
})
- 每个中间件拿到的
dispatch指向整条链的头部,next指向链的当前位置之后。这就是next(action)与dispatch(action)的本质区别:前者“顺流而下”,后者“回到链头重新过一遍所有中间件”。中间件的函数签名定义在 src/types/middleware.ts:
export interface Middleware<...> {
(
api: MiddlewareAPI<D, S>
): (next: (action: unknown) => unknown) => (action: unknown) => unknown
}
即中间件是一个三层高阶函数:store => (next) => (action) => ...,其中 api 提供 getState 和(指向链头的)dispatch。
2.3 测试如何验证“链”与“递归 dispatch”
test/applyMiddleware.spec.ts 中的 'passes recursive dispatches through the middleware chain' 用例直接验证了递归 dispatch 会再次穿过整条链:
store.dispatch(addTodoAsync('Use Redux') as any)
return dispatchedValue.then(() => {
expect(spy.mock.calls.length).toEqual(2)
})
即一个 thunk 类型的 action 异步完成后再次 dispatch 普通 action 时,中间件的 spy 会被调用两次——第一次是初始 dispatch,第二次是 thunk 内部 dispatch 重新从链头进入。这从行为上证实了“dispatch 是回到链头”的语义。另外,中间件内调用 dispatch(action, ...args) 时的额外参数也会透传,由 'passes through all arguments of dispatch calls from within middleware' 用例(test/applyMiddleware.spec.ts)验证。
2.4 实践形态
把多个中间件放进一次 applyMiddleware 调用即可,它们会被组装进同一条链:
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(
thunk, // 处理函数型 action
logger // 记录 action 与 state 变化
)
)
从 src/applyMiddleware.ts 的文档注释还可以看到一个约定:由于中间件可能是异步的,applyMiddleware 应当作为组合链中第一个 store enhancer。
三、如何只订阅状态的一部分?订阅回调能拿到 action 吗?
3.1 store.subscribe 是低级原语
Redux 只提供单一的 store.subscribe 方法用于通知监听者“store 已更新”。注意两个关键 API 事实:
- 监听回调不接收当前 state 作为参数——它只是指示有东西发生了变化。订阅逻辑在回调中调用
getState()获取最新 state; - 监听回调也不会拿到被 dispatch 的 action——如果 action 本身需要被专门处理,应当使用中间件,而不是订阅。
这个 API 被刻意设计为一个无依赖、无复杂性的低级原语(low-level primitive),其上层用途是:UI 绑定库(如 React Redux)可以为每个连接的组件各建一个订阅;也可以自行编写函数智能比较新旧 state,只在某部分变化时执行额外逻辑。FAQ 中提到的社区库(redux-watch、redux-subscribe、redux-subscriber 等)都是在这原语之上的不同封装思路。
为什么 state 不作为参数传给监听者?原文给出的理由是:不传新 state 是为了简化 DevTools 这类 store enhancer 的实现;同时订阅者本应是对 state 值本身做出反应,而不是对 action 做出反应。
3.2 源码印证:订阅快照机制
subscribe 的完整实现在 src/createStore.ts。其中有几个值得注意的机制:
(1)监听者列表在每次 dispatch 前做浅拷贝快照。 ensureCanMutateNextListeners(src/createStore.ts)保证 nextListeners 是 currentListeners 的一份独立拷贝:
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = new Map()
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener)
})
}
}
配合 dispatch 末尾的 const listeners = (currentListeners = nextListeners)(src/createStore.ts),意味着:在监听者执行过程中发生 subscribe/unsubscribe,不会影响正在进行中的这一次 dispatch,但下一次 dispatch(包括嵌套 dispatch)会用到更新的快照。dispatch 的 JSDoc(src/createStore.ts)对此有两条正式约定:
- 订阅列表在每次
dispatch()调用前被快照;dispatch 进行中的 subscribe/unsubscribe 不影响当前这次 dispatch; - 监听者不应期望看到所有状态变化——嵌套
dispatch()可能在监听者被调用前多次更新 state;但可以保证,dispatch 开始前注册的所有订阅者,在 dispatch 退出时都会以最新 state 被调用。
这些约定均有测试覆盖,见 test/createStore.spec.ts 中的多个用例,例如:
'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 使用最新的订阅者快照。
(2)reducer 执行期间禁止 subscribe 与 unsubscribe。 subscribe 与 unsubscribe 内部都会检查 isDispatching 标志(src/createStore.ts):
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. '
)
}
getState() 同样在 isDispatching 期间被禁止调用,提示 reducer 已经通过参数拿到了 state。这些约束共同保证了 dispatch 期间状态的可预测性。
(3)store 创建时会 dispatch 一次 INIT action(src/createStore.ts),让每个 reducer 返回其初始 state,从而“填充”初始状态树;replaceReducer 也会以类似方式 dispatch 一个 REPLACE action 重新填充状态树——这对按需加载 reducer(代码分割、热重载)有用。
3.3 “只订阅一部分状态”怎么做?
由于 subscribe 是低级的“全量变化通知”,部分订阅需要在应用层自行实现,典型模式是:
import { shallowEqual } from 'react-redux' // 或自行实现浅比较
const lastSlice = { value: store.getState().todos }
store.subscribe(() => {
const nextSlice = store.getState().todos
if (!shallowEqual(lastSlice.value, nextSlice)) {
lastSlice.value = nextSlice
// 只对 todos 切片变化做出反应
}
})
即:回调中读取 getState(),比较关心的切片与上次值,只在切片变化时执行逻辑。React Redux 正是用这种“每组件一个订阅 + 选择器 + 相等性检查”的模式在组件层实现了部分订阅。若关心的是哪些 action 触发了变化,正确工具是中间件而非 subscribe——中间件位于 dispatch 链中,天然能拿到 action。
四、小结与延伸阅读
| 问题 | 结论 | 依据 |
|---|---|---|
| 能否多个 Store? | 可以但不应,先试 reducer 组合;例外是 profiling 确认的性能问题、应用级隔离 | docs/faq/StoreSetup.md、src/createStore.ts |
| 能否 import 单例 Store? | 不推荐,会阻碍应用隔离与服务端渲染(每请求需独立 store) | docs/faq/StoreSetup.md、docs/usage/ServerRendering.md |
| 多条中间件链? | 不行,不同链的 dispatch 引用互相断开;所有中间件放进一次 applyMiddleware |
src/applyMiddleware.ts、test/applyMiddleware.spec.ts |
next vs dispatch? |
next 顺链向下,dispatch 回到链头重新过全链 |
src/types/middleware.ts |
| 订阅能拿到 action/state 吗? | 都不传参;回调中调 getState(),action 处理交给中间件 |
src/createStore.ts |
延伸阅读(均为仓库内文档):
- API: Store ——
subscribe/dispatch/getState/replaceReducer的完整 API 说明; - API: applyMiddleware 与 Fundamentals Part 4: Store —— store 与中间件的完整教程;
- Understanding Middleware —— 中间件机制的设计史;
- Structuring Reducers —— 用 reducer 拆分替代多 store 的组合方案;
- 配置 Store 的完整文档 —— enhancer 组合、DevTools 等 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 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