Redux Fundamentals 第 1 部分深入解析:Redux 核心概念、最小计数器应用与 Store 底层实现
本文基于 Redux 官方 Fundamentals 教程第 1 部分展开,系统讲解 Redux 是什么、为什么以及何时使用它,并通过一个零依赖的最小计数器应用完整走查 state、action、reducer、store、dispatch、subscribe 六大核心部件的协作方式;同时结合当前仓库 redux@5.0.1 的核心源码与测试用例,印证 store 初始化时的 @@redux/INIT 调度、action 校验规则和订阅快照机制等底层细节,帮助你在理解概念的同时看清这些规则在实现层是如何被强制执行的。
教程导读:你将学到什么
Redux Fundamentals 是一套面向核心的系列教程,目标是带你理解使用 Redux 的核心概念、原则与模式。读完整个系列后,你应当能够说明一个 Redux 应用的各个组成部分、数据在 Redux 应用中的流动方式,以及构建 Redux 应用的标准推荐模式。
第 1 部分(即本文)先通过一个可运行的最小 Redux 应用示例,让你先认识各个部件;后续的第 2 部分:Redux 概念与数据流会深入这些部件的细节与数据流动;从第 3 部分:状态、动作与 Reducer开始,会用这些知识"手工"构建一个小型示例应用,让你亲眼看到每个环节发生了什么,然后再谈标准模式与抽象,最后把低层示例映射到真实应用中推荐的高层模式(即 Redux Toolkit)。
这套教程教你的是"Redux 如何工作"以及"为什么这些模式存在"。 官方教程中有一个重要提示:教程前半部分刻意不引入 Redux Toolkit,等你理解了一切如何拼装在一起之后,再转向 Redux Toolkit 来简化开发——Redux Toolkit 是构建生产级 Redux 应用的标准方式,它建立在本教程覆盖的全部概念之上。理解了这些核心概念,你就能更高效地使用 Redux Toolkit(背景可参阅 Redux Toolkit 概述 与 为什么今天要用 RTK)。
阅读前提
教程对读者有一定基础假设,以便聚焦于 Redux 本身。官方列出的前置知识包括:
- 熟悉 HTML 与 CSS;
- 熟悉 ES2015(ES6)语法与特性;
- 理解数组与对象的展开运算符(spread operator);
- 熟悉 React 术语:JSX、函数组件(Function Components)、Props、State 与 Hooks;
- 理解异步 JavaScript(Promise 基础)与发起 HTTP 请求(fetch)。
如果对这些主题还不够熟练,官方建议先花些时间打好基础再回来学习 Redux。此外,建议在浏览器中安装 React DevTools 与 Redux DevTools 两个扩展,后续教程会反复用到它们。
Redux 是什么?
官方给出的定义是:Redux 是一个用于管理和更新全局应用状态的"模式 + 库"——UI 触发称为 "action(动作)" 的事件来描述"发生了什么",而独立的更新逻辑 "reducer" 响应这些动作更新状态。 它充当了一个集中式 store,存放需要被整个应用共享的状态,并通过一组规则保证状态只能以可预测的方式被更新。
为什么使用 Redux?
Redux 帮你管理"全局"状态——即应用中许多部分都需要用到的状态。
Redux 提供的模式与工具让你更容易理解状态在何时、何地、为何以及如何被更新,以及应用逻辑在这些变化发生时将如何行为。 Redux 引导你写出可预测、可测试的代码,从而给你"应用会按预期工作"的信心。
什么时候应该使用 Redux?
Redux 帮你处理共享状态管理,但和任何工具一样,它有权衡:要学的概念更多、要写的代码更多,还给代码增加了一层间接性,并要求你遵守某些限制。这是短期生产力与长期生产力之间的取舍。
Redux 在以下场景更有用:
- 应用有大量状态,且在应用很多地方都需要用到;
- 应用状态随时间频繁更新;
- 更新这些状态的逻辑可能相当复杂;
- 代码库规模中等或较大,可能由多人协作开发。
并非所有应用都需要 Redux。 值得花时间思考你要构建的应用类型,判断哪些工具最适合解决你面临的问题。若不确定 Redux 是否合适,可参考仓库内 FAQ 中的何时该用 Redux?章节获取更多指导。
Redux 生态:相关库与工具
Redux 本身是一个独立的纯 JS 库,但通常会与几个配套包一起使用:
Redux Toolkit
Redux Toolkit(RTK) 是官方推荐的编写 Redux 逻辑的方式。它包含了构建 Redux 应用所需的包与函数,内置了官方建议的最佳实践,简化了大多数 Redux 任务,防止常见错误,让编写 Redux 应用更容易。当前仓库的示例应用 examples/counter 就是基于 RTK 的完整示例。
React-Redux
Redux 可以集成任何 UI 框架,最常用的是 React。React-Redux 是官方包,让 React 组件能够与 Redux store 交互:读取状态片段、派发 action 更新 store。
Redux DevTools Extension
Redux DevTools 浏览器扩展显示 store 状态随时间变化的历史,让你能高效调试应用,包括使用"时间旅行调试"(time-travel debugging)等强大技术。
Redux 基础:Store 是应用的核心
现在来看构成 Redux 应用的部件。注意:本教程后续描述聚焦于 Redux 核心库(redux 包)本身,其他相关包会在教程后续部分逐步介绍。
Redux Store
每个 Redux 应用的核心都是 store(仓库)。store 是一个容器,保存着应用的全局 state(状态)。
store 是一个 JavaScript 对象,但它比普通的全局对象多了几个特殊函数和能力:
- 绝不直接修改或改变保存在 Redux store 中的状态;
- 更新状态的唯一方式是创建一个普通的 action 对象来描述"应用中发生了什么",然后把 action **dispatch(派发)**给 store;
- 当一个 action 被派发时,store 运行根 reducer 函数,由它基于旧状态和该 action 计算出新状态;
- 最后,store 通知所有 **subscriber(订阅者)**状态已更新,UI 随之用新数据刷新。
这四个规则不是口头约定,而是由 createStore 的源码直接强制执行的。从 src/createStore.ts 可以看到 store 内部的真实结构:闭包变量 currentReducer、currentState、currentListeners 与一个 isDispatching 布尔标志。外部能拿到的一切都通过返回的对象暴露(见下文的 Store 接口),currentState 本身被封闭在闭包里,从根本上杜绝了"直接改 state"的可能——这正是上面第一条规则的实现层保证。
最小可运行的 Redux 计数器示例
官方教程用一个极简计数器应用展示完整机制。由于 Redux 是零依赖的独立库,示例只需一个 script 标签加载 Redux、用原生 JS 和 HTML 做 UI。当前仓库中就有对应的示例文件 examples/counter-vanilla/index.html,你可以直接查看它与文档中这段代码的对应关系(仓库版本使用 INCREMENT/DECREMENT 作为 action type 字符串、reducer 以 typeof state === 'undefined' 判断初始状态,机制完全一致)。
下面按官方文档的写法逐步拆解。
状态、动作与 Reducer
首先定义描述应用的初始 state 值:
// Define an initial state value for the app
const initialState = {
value: 0
}
这个应用用一个数字追踪计数器的当前值。Redux 应用通常以一个 JS 对象作为 state 的根,其他值放在该对象内部。
然后定义 reducer 函数。reducer 接收两个参数:当前 state 和描述"发生了什么"的 action 对象。应用启动时还没有状态,所以把 initialState 作为这个 reducer 的默认值传入:
// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function counterReducer(state = initialState, action) {
// Reducers usually look at the type of action that happened
// to decide how to update the state
switch (action.type) {
case 'counter/incremented':
return { ...state, value: state.value + 1 }
case 'counter/decremented':
return { ...state, value: state.value - 1 }
default:
// If the reducer doesn't care about this action type,
// return the existing state unchanged
return state
}
}
这里有几条值得展开的约定:
- action 对象永远带有一个
type字段,是一个你提供的、作为该动作唯一名称的字符串。type应该可读,让任何人看到代码就明白它的意思。教程的命名习惯是domain/事件:用'counter'作为 action type 的前半部分,后半部分描述"发生了什么"——计数器被"incremented"(递增),所以 action type 写作'counter/incremented'。这种命名方式在后续 Redux Toolkit 示例中会原样延续,比如 counterSlice.js 中createSlice的name: 'counter'生成的正是counter/increment、counter/decrement这样的 action type。 - 根据 action 的 type,reducer 要么返回一个全新的对象作为新 state,要么原样返回现有 state(不该变化时)。
- 状态更新是不可变(immutable)的:复制现有状态、在副本上修改,而不是直接改动原对象。
"reducer 必须是纯函数"这一要求也写在类型定义的注释里。从 src/types/reducers.ts 的 Reducer 类型声明可以看到:
export type Reducer<
S = any,
A extends Action = UnknownAction,
PreloadedState = S
> = (state: S | PreloadedState | undefined, action: A) => S
即 reducer 的签名就是 (state, action) => 新state,且类型注释明确要求:"它们必须是纯函数——给定相同输入返回完全相同输出的函数,应当没有副作用。这正是热重载和时间旅行等特性得以实现的原因。不要把 API 调用放进 reducer。"
创建 Store
有了 reducer 函数,就可以调用 Redux 库的 createStore API 创建 store 实例:
// Create a new Redux store with the `createStore` function,
// and use the `counterReducer` for the update logic
const store = Redux.createStore(counterReducer)
把 reducer 函数传给 createStore,它会用这个 reducer 生成初始状态,并在之后用同样的逻辑计算每一次更新。
这里有一个源码级的细节值得注意:store 创建的那一刻,createStore 会立即向自己派发一个私有的 @@redux/INIT action,让每个 reducer 返回其初始状态,从而"填充"初始状态树。见 src/createStore.ts#L382-L386:
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)
这个 INIT 类型并非写死的字符串,而是带随机后缀的私有常量,定义在 src/utils/actionTypes.ts:
const ActionTypes = {
INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
REPLACE: `@@redux/REPLACE${/* #__PURE__ */ randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
随机后缀的作用是避免与业务代码中可能出现的同名 action type 冲突,文件头注释也明确提醒"不要在代码中直接引用这些 action type"。此外,store 还支持 replaceReducer 动态替换 reducer 并保留既有状态(见 src/createStore.ts#L320-L336),替换时同样会派发一个类 INIT 效果的 REPLACE action——这是代码分割、动态加载 reducer 和热重载能力的底层支撑。
还有一个与当前版本(redux@5.0.1,见 package.json)相关的重要事实:createStore 已被标记为 deprecated。从 src/createStore.ts#L16-L40 的 JSDoc 注释可以看到,官方推荐使用 @reduxjs/toolkit 的 configureStore 替代 createStore,并建议 redux 核心包仅用于学习目的;若想避免控制台的弃用警告,可改用 legacy_createStore 导入(src/createStore.ts#L427-L489 导出了 legacy_createStore,它只是转发给 createStore)。这也正好印证了教程的立场:手工写是为了"看清发生了什么",而真实项目请用 Redux Toolkit。
UI:读取状态与订阅更新
任何应用中,UI 都要在屏幕上展示现有状态;用户做点什么,应用就更新数据、再按新值重绘 UI。
// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')
// Whenever the store state changes, update the UI by
// reading the latest store state and showing new data
function render() {
const state = store.getState()
valueEl.innerHTML = state.value.toString()
}
// Update the UI with the initial data
render()
// And subscribe to redraw whenever the data changes in the future
store.subscribe(render)
在这个小例子里,UI 只是一段基本 HTML,用一个 <div> 显示当前值。render 函数知道如何调用 store.getState() 获取最新状态并更新 UI;而 store.subscribe() 让你传入一个订阅回调函数,store 每次更新时都会调用它。把 render 作为订阅者传入,就保证 store 更新后 UI 总能拿到最新值。
Redux 本身是与 UI 无关的独立库,可以用在任何 UI 层上;与 React 的集成方式在第 5 部分:UI 与 React中讲解。
从源码看,getState、subscribe、dispatch 构成了 store 的基本三件套,完整的 API 契约定义在 src/types/store.ts 的 Store 接口中,另外还有两个高阶成员:replaceReducer(前述的动态替换)与 [Symbol.observable](与响应式库互操作的最小 observable 接口)。几个关键的实现细节:
getState()在 reducer 执行期间被禁止调用。src/createStore.ts#L166-L176 中,若isDispatching为真,直接抛出"You may not call store.getState() while the reducer is executing"——因为 reducer 已经拿到了 state 参数,应该从顶层 reducer 向下传递,而不是从 store 里现读。subscribe()在 reducer 执行期间同样被禁止(src/createStore.ts#L201-L243),且每次subscribe都返回一个unsubscribe取消函数。- 订阅列表采用快照机制:
ensureCanMutateNextListeners在派发前对监听者集合做浅拷贝(src/createStore.ts#L145-L159),保证在某个 dispatch 的监听者遍历过程中 subscribe/unsubscribe 不会影响本次进行中的派发,只会影响下一次派发。Store接口的 JSDoc 对此有明确契约说明:订阅在每次dispatch()前被快照;即使嵌套 dispatch 期间状态被更新多次,也保证"dispatch 开始时已注册的所有订阅者,在 dispatch 退出前都会以最新状态被调用"。
派发 Action
最后,需要响应用户输入:创建描述"发生了什么"的 action 对象并派发给 store。调用 store.dispatch(action) 时,store 运行 reducer、计算更新后的状态、再运行订阅者刷新 UI。
// Handle user inputs by "dispatching" action objects,
// which should describe "what happened" in the app
document.getElementById('increment').addEventListener('click', function () {
store.dispatch({ type: 'counter/incremented' })
})
document.getElementById('decrement').addEventListener('click', function () {
store.dispatch({ type: 'counter/decremented' })
})
document
.getElementById('incrementIfOdd')
.addEventListener('click', function () {
// We can write logic to decide what to do based on the state
if (store.getState().value % 2 !== 0) {
store.dispatch({ type: 'counter/incremented' })
}
})
document
.getElementById('incrementAsync')
.addEventListener('click', function () {
// We can also write async logic that interacts with the store
setTimeout(function () {
store.dispatch({ type: 'counter/incremented' })
}, 1000)
})
这里派发的 action 让 reducer 把当前计数值加 1 或减 1。你也可以写"只在某个条件为真时才派发"的代码,或者写异步代码在延迟后再派发 action——教程借此说明:"何时派发"的判断逻辑放在 UI 事件层(或后来的 thunk/middleware 中),"如何改状态"的逻辑留在 reducer 里,这就是 Redux 把"发生了什么"与"状态如何变化"分离的模式。
这些约束在 dispatch 的实现里同样逐条落地。src/createStore.ts#L270-L309 的 dispatch 依次执行四道校验:
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. ...'
)
}
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
}
也就是说:action 必须是普通对象(否则报错,并提示你可能需要 redux-thunk 这类中间件来处理函数等派发值);type 不能是 undefined(报错会提示"你可能拼错了 action type 字符串常量");type 必须是字符串;reducer 执行期间禁止再次派发("Reducers may not dispatch actions.")。全部通过后才置 isDispatching = true、用 reducer 计算新状态(try/finally 保证标志恢复),然后遍历快照后的监听者列表逐一通知,最后把 dispatch 进去的 action 原样返回——这个返回值让日志、DevTools 等工具能方便地拿到"这次到底派发了什么"。
上述每一条规则都能在测试套件中找到对应断言。test/createStore.spec.ts 覆盖了:只接受 plain object actions(only accepts plain object actions)、action type 缺失/undefined/非字符串时报错(throws if action type is missing 等)、reducer 内部禁止 dispatch/getState/subscribe/unsubscribe(does not allow dispatch() from within a reducer 等一组用例)、reducer 抛错后 store 仍能恢复(recovers from an error within a reducer)、订阅的多种边界行为(supports multiple subscriptions、notifies only subscribers active at the moment of current dispatch、uses the last snapshot of subscribers during nested dispatch 等)。想验证本文任何一个结论,直接跑这份测试即可。
数据流(Data Flow)
官方教程用一个数据流动图总结 Redux 应用中的数据流动,它表达了:
- 响应用户点击之类的交互,action 被派发;
- store 运行 reducer 函数计算出新状态;
- UI 读取新状态并显示新值。
文章开头的动图(即文档引用的 ReduxDataFlowDiagram)完整演示了这个循环。如果这些部件此刻还不完全清晰也不用担心——带着这张图继续后面的教程,你会看到这些部件如何严丝合缝地拼在一起。
从"手写"到 Redux Toolkit
那个计数器示例很小,但它展示了一个真实 Redux 应用的所有工作部件——后续各部分讲的内容都是对这些基本部件的扩展。文档结尾的官方总结(Summary)可以完整保留为一份速查清单:
- Redux 是管理全局应用状态的库
- Redux 通常与 React-Redux 一起使用,完成 Redux 与 React 的集成
- Redux Toolkit 是编写 Redux 逻辑的标准方式
- Redux 的更新模式把"发生了什么"与"状态如何变化"分离
- Action 是带
type字段的普通对象,描述应用中"发生了什么" - Reducer 是基于"前一状态 + action"计算新状态值的函数
- Redux store 在 action 被 dispatch 时运行根 reducer
- Action 是带
- Redux 采用"单向数据流"的应用结构
- State 描述应用在某一时刻的状况,UI 基于该 state 渲染
- 当应用中发生某事时:UI 派发一个 action;store 运行 reducer,状态按"发生了什么"被更新;store 通知 UI 状态已变化
- UI 基于新状态重新渲染
这条"单向数据流"落到当前仓库的 RTK 示例 examples/counter 中是这样对应的:store.js 用 configureStore({ reducer: { counter: counterReducer } }) 建库;counterSlice.js 用 createSlice 定义 increment/decrement/incrementByAmount 等 reducer 及其对应 action,用 createAsyncThunk 处理异步逻辑(对应教程中 setTimeout 延迟派发的场景),而 incrementIfOdd 则手写为 thunk、通过 selectCount(getState()) 读取状态后条件性派发——与本文手写示例里 if (store.getState().value % 2 !== 0) 的判断逻辑如出一辙,只是换成了更高阶的写法。
小结与下一步
第 1 部分完成了三件事:给出了 Redux 的准确定义与适用边界(大型共享状态、频繁更新、复杂更新逻辑、多人协作的代码库),拆解了最小计数器应用中 state / action / reducer / store / dispatch / subscribe 的完整链路,并对照 redux@5.0.1 的源码确认了这些模式不是软约定而是硬约束——@@redux/INIT 的初始化派发、dispatch 的四重校验、subscribe 快照机制、reducer 执行期间的调用禁区,全部有对应实现与测试用例可查。现在你已经知道一个 Redux 应用的基本部件,可以进入第 2 部分:Redux 概念与数据流,更细致地研究数据如何在 Redux 应用中流动;系列索引见 tutorials-index。
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
