Redux 是一个可预测的状态容器,它把整个应用的状态集中到一个全局对象中,并规定了状态变更的唯一路径:视图触发 Action,Reducer 根据 Action 计算新状态,Store 保存新状态并通知订阅者。对于刚接触 Redux 的开发者来说,最需要理清的就是 Store、Action、Reducer 这三个概念各自的职责边界。本文从最基础的用法出发,用 JavaScript 代码展示它们是如何协作的。

一、为什么 Redux 需要单一数据源和单向数据流
在传统的前端开发中,状态可能散落在各个组件的内部,组件之间通过层层传递回调或者直接操作全局变量来同步数据。当应用规模变大时,这种模式会让状态变化难以追踪,一个数据被多处修改,调试时往往需要同时查看多个文件才能搞清楚数据流。Redux 的核心思路就是把应用状态集中到一个 Store 中,任何组件都不能直接修改这个状态。
状态变更必须遵循单向数据流:组件只能通过 dispatch 一个 Action 来表达“发生了什么”,Reducer 负责根据这个 Action 计算出新的状态,Store 再把新状态保存起来并通知所有订阅者。这种约束牺牲了一部分灵活性,但换来了极强的可预测性。
二、Action:描述状态变化的意图
Action 本质上是一个普通的 JavaScript 对象,它必须包含一个 type 字段,用来描述这次操作的类型。除了 type 之外,Action 还可以携带额外的数据,通常放在 payload 字段中。例如,当用户添加一条待办事项时,可以定义一个 ADD_TODO 类型的 Action。
为了减少手写对象带来的错误,通常会创建一个 Action Creator 函数,它接收参数并返回一个符合规范的 Action 对象。这样做的好处是,类型字符串被封装在函数内部,修改时只需要改一处,同时调用方不需要关心 Action 的具体结构。
const ADD_TODO = 'ADD_TODO';
function addTodo(text) {
return {
type: ADD_TODO,
payload: text
};
}
Action 本身不直接操作状态,它只是描述了“我想添加一条待办”。真正的状态更新逻辑在 Reducer 中完成。把意图和实现分开是 Redux 设计的一个重要特点,它让开发者可以清晰地看到应用里到底发生了哪些操作。
三、Reducer:纯函数与不可变更新
Reducer 是一个纯函数,它接收旧的 state 和一个 action,然后根据 action.type 决定如何返回新的 state。纯函数有两个关键要求:一是不能产生副作用,例如不要修改传入的参数、不要发起网络请求;二是相同的输入必须得到相同的输出。正因为 Reducer 是纯函数,Redux 的状态变化才变得可预测。
Reducer 中最重要的一条规则是:永远不要直接修改旧的 state。如果 state 是一个对象,需要修改某个属性时,应该返回一个新的对象,可以使用对象展开运算符或者 Object.assign 来创建副本。如果 state 中包含数组,更新数组时也要返回一个新数组,例如使用 concat 或展开运算符,而不是直接调用 push。
const initialState = {
todos: []
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: state.todos.concat(action.payload)
};
default:
return state;
}
}
当 action.type 不匹配任何分支时,Reducer 必须返回原 state。如果 reducer 接收到的 state 是 undefined,Redux 会使用默认参数 state = initialState 来初始化状态。保持不可变更新可以让 Redux 的调试工具准确记录每一次状态变化,也便于后续的性能优化。
四、Store:连接 Action 与 Reducer 的枢纽
Store 是由 Redux 提供的 createStore 方法创建的,它接收一个 Reducer 作为参数。Store 保存了应用的完整状态树,并提供几个核心方法:getState 用来获取当前状态,dispatch 用来派发 Action,subscribe 用来订阅状态变化。在大多数情况下,一个应用只有一个 Store。
调用 dispatch 时,Store 会把当前的 state 和传入的 action 交给 Reducer,得到新的 state 后更新内部状态,并触发所有通过 subscribe 注册的监听函数。监听函数通常用来更新视图,在 React 应用中一般由 react-redux 库自动完成这一步,但理解整个流程非常有必要。
import { createStore } from 'redux';
const store = createStore(todoReducer);
store.subscribe(function() {
console.log('当前状态:', store.getState());
});
store.dispatch(addTodo('学习 Redux'));
store.dispatch(addTodo('编写第一个示例'));
上面的代码创建了一个 Store,订阅了状态变化,然后派发了两个 Action。每次 dispatch 之后,控制台都会打印出最新的状态。可以看到,Store 本身并不承担状态计算的职责,它只负责调度和数据保存,真正的逻辑在 Reducer 中。
五、完整协作流程与最小示例
为了更直观地理解三者的关系,可以用一个完整的 JavaScript 示例把前面几部分组合起来。在这个示例中,我们不依赖任何额外的库,只使用 Redux 的核心 API 来演示状态的创建、变更和订阅。
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
function addItem(name) {
return { type: ADD_ITEM, payload: name };
}
function removeItem(name) {
return { type: REMOVE_ITEM, payload: name };
}
const initialItems = [];
function itemsReducer(state = initialItems, action) {
switch (action.type) {
case ADD_ITEM:
return state.concat(action.payload);
case REMOVE_ITEM:
return state.filter(function(item) {
return item !== action.payload;
});
default:
return state;
}
}
const store = createStore(itemsReducer);
store.subscribe(function() {
console.log('最新状态:', store.getState());
});
store.dispatch(addItem('苹果'));
store.dispatch(addItem('香蕉'));
store.dispatch(removeItem('苹果'));
执行这段代码后,控制台会依次打印出三个状态:首先是只包含苹果的数组,然后是包含苹果和香蕉的数组,最后是只包含香蕉的数组。整个过程中没有一个地方直接修改了原数组,每次操作都返回了新的数组。这就是 Redux 最核心的工作方式。
六、常见认知误区与最佳实践
一个常见的误区是把 Action 和 Reducer 混为一谈。Action 只负责描述意图,Reducer 才负责计算新状态。如果在 Action 中直接操作了 Store,或者在 Action Creator 中执行了异步请求,就会破坏 Redux 的数据流。异步操作应该通过中间件来处理,例如 redux-thunk 或 redux-saga,而不是放在 Action Creator 内部直接执行。
另一个值得注意的地方是 Reducer 的拆分。当应用状态越来越复杂时,把所有逻辑写在一个 Reducer 中会让函数体变得非常庞大。此时可以使用 combineReducers 把不同领域的 Reducer 组合起来,每个 Reducer 只负责管理状态树中的一个分支。这样既保持了单一数据源,又让代码结构清晰。初学者应该先熟练掌握 Store、Action、Reducer 的基础用法,再去接触中间件和状态拆分,这样能少走很多弯路。
Redux状态管理StoreAction Reducer修改时间:2026-08-27 05:51:21