首页
/ Redux Essentials 第 1 篇:核心概念、术语体系与单向数据流完整解析

Redux Essentials 第 1 篇:核心概念、术语体系与单向数据流完整解析

2026-09-04 14:05:26作者:裘晴惠Vivianne

本篇基于 Redux 官方 Essentials 教程的第一部分,系统讲解 Redux 的定位、适用场景、核心术语(Action、Reducer、Store、Dispatch、Selector)以及单向数据流的完整工作机制。阅读本文后,你将能够准确说出 Redux 中每个核心概念的职责边界,理解状态更新的完整链路,并结合仓库中的源码实现(src/createStore.ts)与官方 counter 示例(examples/counter)验证这些概念在工程中的真实落地方式。

Redux 单向数据流动图

单向数据流示意图

教程定位:Essentials 与 Fundamentals 的区别

Redux 官方维护了两条完整教程线,在 docs/tutorials/tutorials-index.md 中有明确说明:

  • Redux Essentials 教程:自顶向下(top-down),教你“如何以正确的方式使用 Redux”,全程采用最新推荐的工具与最佳实践——Redux Toolkit 负责逻辑、React-Redux hooks 负责 UI、RTK Query 负责数据获取与缓存。
  • Redux Fundamentals 教程:自底向上(bottom-up),从零开始、不使用任何抽象,讲解“Redux 内部如何工作”以及标准用法模式存在的原因。

官方推荐从 Essentials 开始。本篇是第一部分,覆盖使用 Redux 所需的关键概念与术语;第二部分:Redux 应用结构将考察一个典型的 React + Redux 应用以观察各部件如何拼装;从 第三部分:基础数据流 开始,将用这些知识构建一个具备真实功能的小型社交媒体信息流应用。

前置知识要求

教程假设读者已经掌握以下内容(若尚未熟悉,建议先补齐再学习 Redux):

  • HTML 与 CSS;
  • ES2015 语法与特性;
  • React 术语:JSX、函数式组件、Props、State、Hooks;
  • 异步 JavaScript 与发起 HTTP 请求(fetch);
  • TypeScript 语法与用法的初步理解。

同时建议在浏览器中安装 React DevTools 与 Redux DevTools 两个扩展,以便在后续章节中调试组件树与 store 状态。

Redux 是什么

Redux 是一个用于管理和更新全局应用状态的库与模式:UI 触发称为 “action”(动作)的事件来描述“发生了什么”,而独立的更新逻辑——称为 “reducer”(归约器)——据此更新状态。它充当了一个集中式状态仓库(store),存放需要在整个应用中共享的状态,并带有一组规则,确保状态只能以可预测的方式被更新。

为什么要使用 Redux

Redux 帮助你管理“全局”状态——需要在应用许多部分共享的状态。Redux 提供的模式与工具让你更容易理解应用中的状态何时、何地、为何、如何被更新,以及应用逻辑在这些变化发生时将如何表现。它引导你写出可预测、可测试的代码,从而建立“应用会按预期工作”的信心。

何时应该使用 Redux

Redux 帮助你处理共享状态管理,但和任何工具一样,它存在权衡:需要学习更多概念、编写更多代码,还给代码带来一定间接性,并要求你遵守特定约束。这是短期产出与长期产出之间的取舍。Redux 在以下情形更有用:

  • 应用中存在大量需要在多处使用的应用状态;
  • 应用状态随时间频繁更新;
  • 更新这些状态的逻辑可能很复杂;
  • 应用是中大型代码库,可能由多人共同维护。

并非所有应用都需要 Redux。花时间思考你正在构建的应用类型,决定哪种工具最适合解决你面临的问题。如果对 Redux 是否适合你的应用没有把握,仓库内的 docs/faq/General.md 中“When should I use Redux?”一节给出了更多指引。

Redux 生态:核心库、Redux Toolkit、React-Redux 与 DevTools

Redux 的核心是一个小而独立的 JS 库(当前仓库 package.json 中版本为 5.0.1),它通常与几个配套包一起使用:

Redux Toolkit:官方推荐的编写 Redux 逻辑的方式。它内置了官方建议的最佳实践,简化了大部分 Redux 任务,防止常见错误。值得注意的是,仓库源码中 src/createStore.tscreateStore 的注释明确将其标记为 @deprecated,并说明“configureStore from Redux Toolkit 是 createStore 的改进版本”,鼓励所有用户将 Redux 代码迁移到 Redux Toolkit——这与文档“RTK 是标准写法”的表述完全一致。

React-Redux:Redux 可以与任何 UI 框架集成,最常用于 React。React-Redux 是官方包,让 React 组件通过与 store 交互来读写数据:读取部分状态、分发 action 以更新 store。仓库中的 counter 示例 examples/counter/src/features/counter/Counter.js 正是通过 useSelector 读取状态、useDispatch 分发 action 的典型用法。

Redux DevTools Extension:展示 store 状态随时间变化的历史,支持高效的调试,包括“时间旅行调试”这类强大技术。

状态管理与单向数据流

先看一个小型 React 计数器组件。它用组件状态追踪一个数字,点击按钮时递增:

function Counter() {
  // 状态:一个计数器值
  const [counter, setCounter] = useState(0)

  // 动作:当某事发生时触发状态更新的代码
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // 视图:UI 定义
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

这是一个自包含的应用,由三部分组成:

  • 状态(State):驱动应用的唯一事实来源(source of truth);
  • 视图(View):基于当前状态的 UI 声明式描述;
  • 动作(Actions):基于用户输入在应用中发生的事件,触发状态更新。

这是**“单向数据流”**的一个小型示例:

  • 状态描述应用在某一时点的状况;
  • UI 基于该状态渲染;
  • 当某事发生(如用户点击按钮),状态基于发生了什么被更新;
  • UI 基于新状态重新渲染。

然而,当多个组件需要共享并使用同一份状态、且这些组件位于应用不同位置时,这种简单性就会瓦解。有时可以通过将状态“提升到”父组件来解决,但并非总是有效。一个解决方案是:把共享状态从组件中抽离出来,放到组件树之外的集中位置。此时,整个组件树就成为一个大“视图”,树中任何位置的组件都可以访问状态或触发动作。

通过定义并分离状态管理涉及的各个概念、强制执行维护视图与状态之间独立性的规则,代码就获得了更好的结构与可维护性。这就是 Redux 的基本思想:一个集中存放应用全局状态的单一位置,加上更新该状态时必须遵循的特定模式,使代码保持可预测

不可变性(Immutability)

“Mutable”意为“可变更”的;如果某物是“immutable”的,它就永远不能被更改。JavaScript 的对象与数组默认都是可变的:

const obj = { a: 1, b: 2 }
// 外部仍是同一个对象,但内容已经改变
obj.b = 3

const arr = ['a', 'b']
// 同样,我们可以修改这个数组的内容
arr.push('c')
arr[1] = 'd'

这称为**变异(mutating)**对象或数组——它是内存中同一个引用,只是内部内容变了。

为了以不可变方式更新值,你的代码必须对现有对象/数组做“拷贝”,然后修改拷贝。可以用 JavaScript 的展开运算符以及返回新数组副本而非变异原数组的数组方法手工完成:

const obj = {
  a: {
    // 要安全地更新 obj.a.c,必须逐层拷贝
    c: 3
  },
  b: 2
}

const obj2 = {
  // 拷贝 obj
  ...obj,
  // 覆盖 a
  a: {
    // 拷贝 obj.a
    ...obj.a,
    // 覆盖 c
    c: 42
  }
}

const arr = ['a', 'b']
// 创建 arr 的新副本,末尾追加 "c"
const arr2 = arr.concat('c')

// 或者,对原数组做一份拷贝:
const arr3 = arr.slice()
// 然后变异这份拷贝:
arr3.push('c')

React 和 Redux 都期望所有状态更新都是不可变地完成的。这一点在 React 的更新检测(依赖引用比较)与 Redux 的 reducer 规则中都至关重要,教程后续会讲解更容易编写不可变更新逻辑的方式(如 Redux Toolkit 基于 Immer 的写法)。

核心术语详解

Actions(动作)

一个 action 是带有 type 字段的普通 JavaScript 对象。可以把 action 理解为描述应用中“发生了某事”的事件

type 字段应是一个描述性的字符串,例如 "todos/todoAdded"。通常按 "domain/eventName"(域/事件名)格式书写:前半部分是该 action 所属的功能或类别,后半部分是具体发生了什么。

action 对象还可以携带其他字段来补充描述发生了什么,按照惯例,这些信息放在名为 payload 的字段中:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Action Creators(动作创建器)

action creator 是创建并返回 action 对象的函数。使用它就不用每次都手写 action 对象:

const addTodo = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}

Reducers(归约器)

reducer 是一个接收当前 stateaction 对象、判断是否需要更新状态、并返回新状态的函数:(state, action) => newState可以把 reducer 理解为事件监听器,它根据收到的 action(事件)类型来处理事件。

“Reducer”这个名字来源于与传给 Array.reduce() 方法的回调函数类似。

Reducer 必须始终遵守以下规则:

  • 只能基于 stateaction 两个参数计算新状态值;
  • 不允许修改现有 state,必须通过拷贝现有状态并修改拷贝值来做不可变更新
  • 必须是“纯”函数——不能执行异步逻辑、计算随机值或触发其他“副作用”。

reducer 函数内部的逻辑通常遵循同样的步骤序列:

  • 检查 reducer 是否关心这个 action;
    • 若关心,拷贝状态,在拷贝上写入新值并返回;
  • 否则,原样返回现有状态。
const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // 检查 reducer 是否关心这个 action
  if (action.type === 'counter/increment') {
    // 关心:拷贝 `state`
    return {
      ...state,
      // 在拷贝上更新为新值
      value: state.value + 1
    }
  }
  // 否则:原样返回现有状态
  return state
}

Reducer 内部可以使用任意逻辑(if/elseswitch、循环等)来决定新状态应该是什么。

仓库源码 src/combineReducers.ts 中的 assertReducerShape 会在初始化时用代码强制执行上面这些规则:它以 reducer(undefined, { type: INIT }) 探测初始状态,若返回 undefined 则抛出“slice reducer 在初始化时返回了 undefined”的错误;再用随机 type 探测,若仍返回 undefined 则抛出“不要处理 redux/* 命名空间下的私有 action,对未知 action 必须返回当前状态”的错误。这正是“reducer 必须对未知 action 原样返回状态、对 undefined 状态返回初始状态”这一规则的运行时保障。

深入理解:为什么叫 “Reducers”?

Array.reduce() 方法可以接收一个值数组,逐项处理并返回一个最终结果,可理解为“把数组归约成一个值”。它接收一个回调函数,对数组每一项调用一次,回调有两个参数:

  • previousResult:回调上一次返回的值;
  • currentItem:数组中当前项。

回调首次执行时没有 previousResult,因此需要额外传入一个初始值作为第一次的 previousResult。例如对一组数字求和:

const numbers = [2, 5, 8]

const addNumbers = (previousResult, currentItem) => {
  console.log({ previousResult, currentItem })
  return previousResult + currentItem
}

const initialValue = 0

const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total)
// 15

注意 addNumbers 这个“reduce 回调”自身无需维护任何状态:接收 previousResultcurrentItem,做点处理,返回新结果。

Redux 的 reducer 函数与这个“reduce 回调”是完全相同的思想:接收“上一次结果”(state)与“当前项”(action 对象),据此决定新的状态值并返回。如果把一串 Redux action 组成数组、调用 reduce() 并传入 reducer 函数,会得到同样方式的结果:

const actions = [
  { type: 'counter/increment' },
  { type: 'counter/increment' },
  { type: 'counter/increment' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

可以说,Redux reducer 是把一组(随时间产生的)action 归约成单一状态。区别在于:Array.reduce() 一次性完成,而 Redux 中这个过程贯穿整个应用的生命周期。

Store(仓库)

当前 Redux 应用状态存放在一个称为 store 的对象中。store 通过传入 reducer 创建,并带有 getState 方法返回当前状态值:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

仓库源码 src/createStore.ts 印证了“store 创建时会调用一次 reducer”的机制:创建 store 后立即 dispatch 一个私有的 @@redux/INIT action,使每个 reducer 都返回其初始状态,从而填充初始状态树。这些私有 action type 定义在 src/utils/actionTypes.ts,带有随机字符串后缀(如 @@redux/INIT${randomString()}),文档注释明确写着“这些是 Redux 保留的私有 action 类型,请勿在代码中直接引用”。

Dispatch(分发)

store 带有 dispatch 方法。更新状态的唯一方式是调用 store.dispatch() 并传入 action 对象。store 会运行 reducer 函数并把返回值保存为新状态,之后再调用 getState() 即可取到更新后的值:

store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}

可以把 dispatch action 理解为“触发一个应用事件”:某事发生了,我们希望 store 知道。Reducer 如同事件监听器,听到自己感兴趣的 action 就相应地更新状态。通常调用 action creator 来分发正确的 action:

const increment = () => {
  return {
    type: 'counter/increment'
  }
}

store.dispatch(increment())

console.log(store.getState())
// {value: 2}

src/createStore.tsdispatch 实现可以看到,这一“唯一更新通道”是被源码硬性约束的:

  • action 必须是普通对象isPlainObject 校验),否则抛出 “Actions must be plain objects”,并提示如需分发函数(thunk)需引入中间件;
  • action.type 不能是 undefined,且必须是字符串,否则分别抛出 “Actions may not have an undefined 'type' property” 与 “Action 'type' property must be a string”;
  • 在 reducer 执行期间(isDispatching 为真时)再次 dispatch 会抛出 “Reducers may not dispatch actions.”,这与“reducer 必须是纯函数、不能有副作用”的规则相呼应;
  • dispatch 完成状态计算后,遍历监听器快照逐一通知订阅者,最后返回被分发的 action 本身(方便 dispatch 链式返回值透传)。

测试用例 test/createStore.spec.ts 中也包含对“reducer 执行期间调用 getState() / subscribe() / 嵌套 dispatch 应抛出错误”的专门验证,印证了上述约束在实现层面是真实生效的。

Selectors(选择器)

selector 是知道如何从 store 状态值中提取特定信息的函数。随着应用变大,它们可以避免不同部分读取同一份数据时重复逻辑:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

counter 示例中 examples/counter/src/features/counter/counterSlice.js 同样定义了 selectCount,并说明 selector 也可以内联在使用处,例如 useSelector(state => state.counter.value)

Redux 应用的数据流

前面讲过“单向数据流”,它描述了更新应用的步骤序列:

  • 状态描述应用在某一时点的状况;
  • UI 基于该状态渲染;
  • 当某事发生(如用户点击按钮),状态基于发生了什么被更新;
  • UI 基于新状态重新渲染。

对 Redux 而言,可以把这些步骤拆得更细:

初始化阶段:

  • 使用一个根 reducer(root reducer)函数创建 Redux store;
  • store 调用一次根 reducer,把返回值保存为初始 state(对应源码中 src/createStore.ts 处的 dispatch({ type: ActionTypes.INIT }));
  • UI 首次渲染时,组件访问 store 的当前状态,用这些数据决定渲染什么,同时订阅未来的 store 更新,以便知道状态是否变化。

更新阶段:

  • 应用中发生某事,如用户点击按钮;
  • 应用代码向 store 分发一个 action,如 dispatch({type: 'counter/increment'})
  • store 用上一个 state当前 action 再次运行 reducer,把返回值保存为新 state(对应 src/createStore.tscurrentState = currentReducer(currentState, action));
  • store 通知所有已订阅的 UI 部分“store 已更新”;
  • 每个需要从 store 取数的组件检查其所需的部分状态是否变化;
  • 每个发现自身数据变化的组件强制用新数据重新渲染,从而更新屏幕内容。

这一完整闭环与文首的 Redux 数据流动图(website/static/img/tutorials/essentials/ReduxDataFlowDiagram.gif)一一对应。

官方 counter 示例中的完整落地

仓库 examples/counter 示例把上述概念串成了一条可运行的链路:

  • examples/counter/src/app/store.js:用 configureStore 创建 store,并通过 reducer 映射对象注册 counter slice;
  • examples/counter/src/features/counter/counterSlice.jscreateSlice 基于 name: 'counter' 自动生成 counter/increment 等 action creator,并在 reducer 中直接写“类变异”逻辑(state.value += 1)——这正是前面不可变性一节的延伸:Redux Toolkit 借助 Immer 库在底层完成不可变更新;
  • examples/counter/src/features/counter/Counter.js:组件用 useSelector(selectCount) 订阅状态、用 dispatch(increment()) 触发更新,完整体现“状态 → 视图 → 事件 → 新状态 → 重渲染”的单向数据流。

本篇小结

Redux 引入了不少新术语与概念。作为回顾,本篇覆盖的内容是:

  • Redux 是管理全局应用状态的库
    • 通常与 React-Redux 库一起使用,以集成 Redux 和 React;
    • Redux Toolkit 是编写 Redux 逻辑的标准方式(仓库源码中 createStore 的弃用注释也在强调这一点)。
  • Redux 的更新模式把“发生了什么”与“状态如何变化”分离
    • Action 是带 type 字段的普通对象,描述应用中“发生了什么”;
    • Reducer 是依据前一状态 + action 计算出新状态值的函数;
    • Redux store 在每次 action 被 dispatch 时运行根 reducer。
  • Redux 采用“单向数据流”应用结构
    • 状态描述应用某一时点的状况,UI 基于该状态渲染;
    • 当应用中发生某事:
      • UI 分发一个 action;
      • store 运行 reducer,状态基于发生了什么被更新;
      • store 通知 UI 状态已变化;
    • UI 基于新状态重新渲染。

下一步

我们已逐一认识了 Redux 应用的各个组成部分。接下来请继续阅读 第二部分:Redux Toolkit 应用结构,通过一个完整可运行的例子观察各部件如何拼装在一起。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384