Redux 基础,第二部分:核心概念与数据流
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
- 使用 Redux 的关键术语与概念
- 数据如何在 Redux 应用中流动
简介
在第一部分:Redux 概述中,我们讨论了 Redux 是什么、为何需要使用它,并列举了通常与 Redux 核心库配合使用的其他工具库。我们还通过一个小型示例展示了 Redux 应用的实际形态及其构成要素。最后简要介绍了 Redux 中的关键术语与概念。
本部分我们将深入解析这些术语与概念,并详细阐述数据如何在 Redux 应用中流动。
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
请注意,本教程有意展示旧式的 Redux 逻辑模式(这些模式比我们当前推荐的 "现代 Redux" 模式需要编写更多代码),目的是为了解释 Redux 背后的原理和概念。它_并非_用于生产环境的项目。
学习使用 Redux Toolkit 实现 "现代 Redux" 模式,请参考以下文档:
- 完整的 "Redux 必备教程":使用 Redux Toolkit 教授"如何以正确方式使用 Redux"构建真实应用。我们推荐所有 Redux 学习者阅读此教程!
- Redux 基础教程,第 8 部分:使用 Redux Toolkit 的现代 Redux:展示如何将前面章节的底层示例转换为现代 Redux Toolkit 实现
背景概念
在编写实际代码前,我们先了解使用 Redux 需要掌握的核心术语与概念。
状态管理
首先观察一个简单的 React 计数器组件。它在组件状态中跟踪数字,并在按钮点击时递增:
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
这是一个包含以下部分的自包含应用:
-
状态(state):驱动应用的真实数据源
-
视图(view):基于当前状态的 UI 声明式描述
-
动作(actions):用户输入触发的事件,引发状态更新
这体现了**"单向数据流"**的简单示例:
-
状态描述应用在特定时间点的状况
-
UI 基于该状态渲染
-
当事件发生时(如用户点击按钮),根据事件内容更新状态
-
UI 基于新状态重新渲染

然而,当有多个组件需要共享并使用同一状态时,尤其是当这些组件位于应用的不同部分时,这种简单性可能会被打破。有时可以通过状态提升到父组件来解决,但这并非总是有效。
一种解决方案是将共享状态从组件中提取出来,置于组件树之外的中心化位置。这样,我们的组件树就变成了一个大型"视图",任何组件无论位于树中何处,都能访问状态或触发动作!
通过定义并分离状态管理相关的概念,并强制执行保持视图与状态独立性的规则,我们让代码获得了更好的结构和可维护性。
这就是 Redux 的核心思想:一个中心化的位置存储应用的全局状态,以及更新状态时需要遵循的特定模式,使代码行为可预测。
不可变性
"可变"(Mutable)意味着"可更改"。如果某物是"不可变"(Immutable)的,则永远无法被修改。
JavaScript 对象和数组默认都是可变的。创建对象后可以修改其字段内容,创建数组后同样可以更改其元素:
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3
const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'
这称为对象或数组的"突变"(mutating)。内存中的对象或数组引用未变,但其内部内容已被更改。
要以不可变方式更新值,代码必须创建现有对象/数组的副本,然后修改这些副本。
我们可以使用 JavaScript 的数组/对象展开运算符,以及返回新数组副本而非修改原数组的数组方法来实现:
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')
Redux 要求所有状态更新都通过不可变方式完成。稍后我们将探讨这一原则的重要性及其应用场景,并介绍简化不可变更新逻辑的方法。
关于 JavaScript 中不可变性工作原理的更多信息,请参阅:
Redux 核心术语
在继续之前,需要先熟悉以下关键 Redux 术语:
动作(Actions)
动作是包含 type 字段的普通 JavaScript 对象。可将动作视为描述应用中发生事件的对象。
type 字段应为描述性字符串(如 "todos/todoAdded")。我们通常按 "domain/eventName" 格式命名,第一部分表示动作所属功能类别,第二部分描述具体事件。
动作对象可包含其他事件相关信息字段。按约定,这类信息应放在名为 payload 的字段中。
典型动作对象示例如下:
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
归约器(Reducers)
归约器是接收当前 state 和 action 对象的函数,决定如何更新状态(如有必要)并返回新状态:(state, action) => newState。可将归约器视为基于接收动作(事件)类型处理事件的事件监听器。
"归约器"函数得名于其类似 Array.reduce() 方法的回调函数。
Reducer 必须始终遵循一些特定规则:
-
它们应该只根据
state和action参数来计算新的状态值 -
不允许直接修改现有的
state。相反,必须通过复制现有state并对副本进行修改,实现 不可变更新。 -
禁止执行异步逻辑、生成随机值或引发其他"副作用"
稍后我们将详细讨论 reducer 的规则,包括其重要性和正确遵循方法
Reducer 函数内部的逻辑通常遵循相同的步骤序列:
-
检查该 reducer 是否关心此 action
- 如果是,则复制当前状态,用新值更新副本后返回
-
否则直接返回现有状态不作更改
以下是一个小型 reducer 示例,展示了每个 reducer 应遵循的步骤:
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
Reducer 内部可以使用任何逻辑决定新状态:if/else、switch、循环等
Detailed Explanation: Why Are They Called 'Reducers?'
The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as "reducing the array down to one value".
Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:
previousResult, the value that your callback returned last timecurrentItem, the current item in the array
The first time that the callback runs, there isn't a previousResult available, so we need to also pass in an initial value that will be used as the first previousResult.
If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:
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
Notice that this addNumbers "reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new result value.
A Redux reducer function is exactly the same idea as this "reduce callback" function! It takes a "previous result" (the state), and the "current item" (the action object), decides a new state value based on those arguments, and returns that new state.
If we were to create an array of Redux actions, call reduce(), and pass in a reducer function, we'd get a final result the same way:
const actions = [
{ type: 'counter/incremented' },
{ type: 'counter/incremented' },
{ type: 'counter/incremented' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}
We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.
存储(Store)
当前 Redux 应用状态保存在称为 store 的对象中
通过传入 reducer 创建 store,其提供 getState 方法获取当前状态值:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
分发(Dispatch)
Redux store 提供 dispatch 方法。更新状态的唯一方式是调用 store.dispatch() 并传入 action 对象。store 将执行 reducer 函数并保存新状态值,之后可通过 getState() 获取更新后的值:
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
可将 dispatch action 视为在应用中"触发事件"。当事件发生后,我们需要通知 store。Reducer 如同事件监听器,当收到关注的 action 时,会响应更新状态
选择器(Selectors)
Selector 是知道如何从 store 状态值提取特定信息的函数。随着应用规模增大,这能避免不同模块读取相同数据时重复逻辑:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
核心概念与原则
Redux 的设计理念可归纳为三大核心原则:
单一数据源
应用的全局状态以对象形式存储在单一**存储(store)**中。任何数据都应仅存在于一个位置,而非多处重复。
这种设计使状态变化时的调试和检查更加便捷,同时集中了需要与整个应用交互的逻辑。
这并_不_意味着应用中的_所有_状态都必须存入 Redux!应根据实际使用场景,决定状态应存放在 Redux 还是 UI 组件中。
状态只读
改变状态的唯一方式是分发动作(action)——描述发生事件的对象。
这种方式避免了 UI 意外覆盖数据,并便于追踪状态更新原因。由于动作是普通 JS 对象,可被记录、序列化、存储,并在后续回放用于调试或测试。
变更通过纯归约器实现
通过编写**归约器(reducer)**函数指定如何基于动作更新状态树。归约器是纯函数,接收先前状态和动作,返回新状态。可将其拆分为更小的辅助函数,或编写可复用的通用归约器。
Redux 应用数据流
此前我们提到的"单向数据流"描述了应用更新的步骤序列:
-
状态描述应用在特定时间点的状况
-
UI 基于该状态渲染
-
当事件发生时(如用户点击按钮),根据事件内容更新状态
-
UI 基于新状态重新渲染
对于 Redux,我们可以将这些步骤细化为:
-
初始化阶段:
- 使用根 reducer 函数创建 Redux store
- store 执行根 reducer 并将返回值保存为初始
state - 首次渲染 UI 时,组件访问 Redux store 当前状态决定渲染内容,同时订阅后续 store 更新以感知状态变化
-
更新流程:
- 应用中发生事件(如用户点击按钮)
- 应用代码向 Redux 存储库分发动作,如
dispatch({type: 'counter/incremented'}) - 存储库使用先前
state和当前action再次运行归约器函数,并将返回值保存为新state - 存储库通知所有订阅的 UI 部分已完成更新
- 每个需要存储库数据的 UI 组件检查所需状态部分是否变化
- 检测到数据变化的组件强制使用新数据重新渲染,从而更新屏幕显示
以下是该数据流的可视化展示:

学习要点
- Redux 设计理念可总结为三大原则
- 全局应用状态保存在单一存储库中
- 存储库状态对应用其他部分只读
- 使用归约器函数响应动作更新状态
- Redux 采用"单向数据流"应用结构
- 状态描述应用在特定时刻的状况,UI 基于该状态渲染
- 当应用发生事件时:
- UI 分发动作
- 存储库运行归约器,根据事件内容更新状态
- 存储库通知 UI 状态已变更
- UI 基于新状态重新渲染
下一步是什么?
现在您应已熟悉描述 Redux 应用各核心模块的关键概念与术语。
接下来,我们将通过第三部分:状态、动作与归约器构建新的 Redux 应用,了解这些模块如何协同工作。