Redux 核心概念:Store、Action、Reducer、单向数据流、纯函数与不可变数据
说实话,Redux 刚出来那会儿,我也觉得它挺绕的。什么 Store、Action、Reducer,一堆新名词。但用久了你会发现,它其实就三板斧。今天咱们把这几个核心概念掰开揉碎,讲清楚。
1. Store:唯一的数据源
Store 就是整个应用的状态仓库。我习惯把它想象成一个「中央数据库」——所有组件需要共享的数据,都放在这里。
在 Redux 里,Store 有且只有一个。这一点很重要。为什么?因为多个 Store 会导致数据同步困难,你想想看,两个 Store 里的数据打架了,你找谁评理去?
核心特征:
- 单一数据源(Single Source of Truth)
- 通过
createStore()创建 - 提供
getState()、dispatch()、subscribe()三个方法
import { createStore } from 'redux'
// 创建一个 Store
const store = createStore(rootReducer)
// 获取当前状态
console.log(store.getState())
// 订阅状态变化
store.subscribe(() => {
console.log('状态变了:', store.getState())
})
我在项目中遇到过一个问题:有人把多个不相关的数据放在同一个 Store 里,结果 reducer 越写越臃肿。后来我学乖了,用 combineReducers 拆分模块,每个模块管好自己的那一亩三分地。
2. Action:描述「发生了什么」
Action 就是一个普通的 JavaScript 对象。它必须有一个 type 属性,用来描述你要做什么事。
// 一个典型的 Action
const addTodoAction = {
type: 'ADD_TODO',
payload: {
id: 1,
text: '学习 Redux',
completed: false
}
}
嗯,这里要注意:Action 只描述「发生了什么」,不描述「怎么处理」。它就像一个快递单,只告诉你要送什么货,至于怎么分拣、怎么配送,那是别人的事。
个人习惯:我一般用常量来定义 Action Type,比如 const ADD_TODO = 'ADD_TODO'。这样能避免拼写错误,也方便全局搜索。
3. Reducer:决定「状态怎么变」
Reducer 是一个纯函数。它接收当前状态和一个 Action,然后返回一个新的状态。
const initialState = {
todos: []
}
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
}
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
}
default:
return state
}
}
为什么叫 Reducer?因为它和 JavaScript 的 Array.reduce() 是一个道理——把一堆 Action 逐个「归约」成最终的状态。
我曾经踩过的坑:在 Reducer 里直接修改了 state。比如 state.todos.push(newTodo)。结果 UI 死活不更新,排查了半天才发现是可变数据的问题。记住:永远不要修改原来的 state,要返回一个新的对象。
4. 纯函数与不可变数据
这两个概念是 Redux 的基石。说白了:
- 纯函数:同样的输入,永远得到同样的输出。没有副作用,不修改外部变量。
- 不可变数据:数据一旦创建,就不能被修改。要改变就创建一个新的。
你想想看,如果 Reducer 不是纯函数,那同样的 Action 在不同时间执行,可能得到不同的结果。这调试起来得多痛苦?
不可变数据的常见操作:
| 操作 | 可变方式(不要用) | 不可变方式(推荐) |
|---|---|---|
| 添加数组元素 | arr.push(item) |
[...arr, item] |
| 删除数组元素 | arr.splice(index, 1) |
arr.filter((_, i) => i !== index) |
| 更新对象属性 | obj.key = value |
{...obj, key: value} |
| 嵌套对象更新 | obj.a.b.c = value |
{...obj, a: {...obj.a, b: {...obj.a.b, c: value}}} |
我个人习惯用 Immer 这个库来处理不可变数据。它让你可以用「可变」的写法,生成「不可变」的结果。代码会清爽很多。
5. 单向数据流
这是 Redux 最优雅的设计之一。数据流动的方向是固定的,就像一条单行道:
- 用户触发一个事件(比如点击按钮)
- 组件 dispatch 一个 Action
- Store 把 Action 传给 Reducer
- Reducer 根据 Action 计算出新的 State
- Store 通知所有订阅者(组件)状态已更新
- 组件重新渲染
为什么会这样设计?说白了,就是为了让数据变化「可预测」、「可追踪」。你想想看,如果数据可以到处乱改,那 bug 出现的时候,你根本不知道是谁改的。
调试利器:配合 Redux DevTools,你可以看到每一次 dispatch 的 Action、前后的 State 变化。我曾经靠这个工具,半小时定位了一个困扰团队两天的 bug。
6. 把它们串起来
最后,咱们看一个完整的例子,把 Store、Action、Reducer 串起来:
// 1. 定义 Action Type
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
// 2. 创建 Action Creator
const increment = () => ({ type: INCREMENT })
const decrement = () => ({ type: DECREMENT })
// 3. 定义 Reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 }
case DECREMENT:
return { count: state.count - 1 }
default:
return state
}
}
// 4. 创建 Store
const store = createStore(counterReducer)
// 5. 订阅状态变化
store.subscribe(() => console.log('当前计数:', store.getState().count))
// 6. 派发 Action
store.dispatch(increment()) // 当前计数: 1
store.dispatch(increment()) // 当前计数: 2
store.dispatch(decrement()) // 当前计数: 1
你看,整个流程非常清晰。数据从 Store 流出,经过组件渲染,用户操作后通过 Action 流回 Store,再由 Reducer 处理更新。这就是单向数据流的精髓。
总结一下:
- Store 是唯一的数据仓库
- Action 描述发生了什么
- Reducer 根据 Action 更新状态
- 纯函数和不可变数据保证可预测性
- 单向数据流让调试变得简单
嗯,这些概念刚开始可能有点抽象。但你在项目里用上两三次,就会觉得「原来就这么回事」。下一章咱们聊聊中间件,那才是 Redux 真正强大的地方。