
一句话:Redux 用一个单一 store 保存整棵状态树,通过派发 action(描述发生了什么)、由纯函数 reducer 计算新状态来更新——整个流程是单向且可预测的。
一、三个核心概念
① Store(仓库)
一个对象,保存整个应用的状态树。
提供 getState() / dispatch() / subscribe()。
② Action(动作)
一个「描述发生了什么」的普通对象,必须有 type 字段。
{ type: 'todos/add', payload: { id: '1', text: '学习' } }
③ Reducer(归约器)
一个纯函数:(state, action) => newState
根据 action 决定「如何计算新状态」。
关系:dispatch(action) → reducer 计算 → 得到新 state → 通知订阅者
// 最小 Redux 实现(理解原理)
function createStore(reducer, preloadedState) {
let state = preloadedState;
const listeners = new Set();
return {
getState: () => state,
dispatch: (action) => {
state = reducer(state, action); // ⭐ 用 reducer 算新状态
listeners.forEach((l) => l()); // 通知订阅者
return action;
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// 使用
function 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;
}
}
const store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'increment' }); // { count: 1 }
二、单向数据流
┌─────────────────────────────────────────────────────┐
│ │
│ ① UI 触发事件(点击按钮) │
│ ↓ │
│ ② dispatch(action)(描述「发生了什么」) │
│ ↓ │
│ ③ reducer(state, action) → newState(纯函数计算) │
│ ↓ │
│ ④ store 更新并通知所有订阅者 │
│ ↓ │
│ ⑤ 组件用 selector 取值 → 重新渲染 │
│ ↓ │
└──── ⑥ 用户再次交互 ──────────────────────────────────┘
与「组件直接改状态」的区别:
❌ 双向绑定式的更新:
组件 A 改了一个变量 → 谁受影响不确定 → 难以追踪
✅ Redux 的单向流:
所有更新都必须经过 dispatch(action) → reducer
→ 「谁改的、改了什么」全部有记录(可 DevTools 回放)
→ 状态变化是可预测、可复现的
三、Redux 的三个原则
| 原则 | 含义 | 收益 |
|---|---|---|
| 单一数据源 | 整个应用的状态保存在一个 store 里 | 便于调试、序列化、SSR |
| State 是只读的 | 唯一改变状态的方式是 dispatch(action) |
状态变化集中可控,不会「谁都能改」 |
| 使用纯函数执行修改 | reducer 必须是纯函数 | 相同输入得到相同输出 → 可测试、可回放 |
// ② 「state 只读」的含义:不能这样
store.getState().todos.push(newTodo); // ❌ 直接改
// ✅ 必须 dispatch
store.dispatch({ type: 'todos/add', payload: newTodo });
四、为什么 reducer 必须是纯函数
// ✅ 纯函数:相同输入 → 相同输出、无副作用、不修改参数
function todosReducer(state = [], action) {
switch (action.type) {
case 'add':
return [...state, action.payload]; // ⭐ 返回新数组
case 'remove':
return state.filter((t) => t.id !== action.payload);
default:
return state;
}
}
// ❌ 不纯的写法
function badReducer(state = [], action) {
state.push(action.payload); // ❌ 直接修改参数
const id = Math.random(); // ❌ 不确定
localStorage.setItem('todos', JSON.stringify(state)); // ❌ 副作用
return state;
}
| 要求 | 原因 |
|---|---|
| 不修改参数 | Redux 靠引用比较判断状态是否变化;且要支持「时间旅行」 |
| 无副作用 | reducer 可能被执行多次(StrictMode、DevTools 回放),副作用会重复 |
| 结果可预测 | 保证「相同输入 → 相同输出」,DevTools 的回放才能正确 |
注意:Redux DevTools 的「时间旅行」正是依赖「reducer 是纯函数 + 状态不可变」——它保存每一次 action 与状态快照,可以「回放」整个历史。如果 reducer 不纯,回放就会产生不同的结果。
五、不可变更新(手写 vs Immer)
// ===== 手写不可变更新 =====
// 对象:展开
{ ...state, user: { ...state.user, name: 'new' } }
// 数组:用非破坏性方法
[...state.items, newItem] // 增
state.items.filter((i) => i.id !== id) // 删
state.items.map((i) => (i.id === id ? { ...i, done: true } : i)) // 改
// 深层嵌套会很难写
return {
...state,
entities: {
...state.entities,
users: {
...state.entities.users,
[id]: { ...state.entities.users[id], name: 'new' },
},
},
};
// ===== 用 Immer(RTK 内置)=====
import { produce } from 'immer';
const next = produce(state, (draft) => {
draft.entities.users[id].name = 'new'; // ⭐ 写法是「直接改」
});
// Immer 通过 Proxy 拦截修改,自动生成新的不可变对象
// RTK 的 createSlice 已经内置了 Immer,所以可以直接写
reducers: {
updateName(state, action) {
state.entities.users[action.payload.id].name = action.payload.name; // ✅ 合法
},
}
| 方式 | 可读性 | 性能 | 适合 |
|---|---|---|---|
| 手写展开 | 深嵌套时很差 | 好 | 浅层结构、库外部使用 |
| Immer | 好(像普通赋值) | 略慢(有 Proxy 开销) | 推荐(RTK 默认) |
| Immutable.js | 好 | 好 | 大量大数据结构(但生态已边缘化) |
⚠️ Immer 的两个注意事项:
① 不能「既修改 draft 又返回新值」
❌ (draft) => { draft.a = 1; return { ...draft }; }
✅ (draft) => { draft.a = 1; } 或 ✅ () => ({ a: 1 })
② 不要「把 draft 存到外部」——它在 produce 结束后就失效了
六、完整例子(不借助 RTK,理解原理)
// ① action types
const ADD_TODO = 'todos/add';
const TOGGLE_TODO = 'todos/toggle';
const SET_FILTER = 'filter/set';
// ② actions(action creator)
const addTodo = (text) => ({ type: ADD_TODO, payload: { id: nanoid(), text, done: false } });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
const setFilter = (filter) => ({ type: SET_FILTER, payload: filter });
// ③ reducers(按领域拆分,最后用 combineReducers 合并)
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
case TOGGLE_TODO:
return state.map((t) => (t.id === action.payload ? { ...t, done: !t.done } : t));
default:
return state;
}
}
function filterReducer(state = 'all', action) {
switch (action.type) {
case SET_FILTER:
return action.payload;
default:
return state;
}
}
const rootReducer = combineReducers({
todos: todosReducer,
filter: filterReducer,
});
// ④ store
const store = createStore(rootReducer);
// ⑤ 选择器(把「怎么取数据」封装起来)
const selectTodos = (state) => state.todos;
const selectFilter = (state) => state.filter;
// ⑥ 派生数据用记忆化选择器(避免每次渲染重算)
import { createSelector } from 'reselect';
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active': return todos.filter((t) => !t.done);
case 'done': return todos.filter((t) => t.done);
default: return todos;
}
}
);
// ⭐ 只有 todos 或 filter 变化时才重新计算
// ⑦ React 里使用(React-Redux)
import { Provider, useSelector, useDispatch } from 'react-redux';
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
function TodoApp() {
const todos = useSelector(selectVisibleTodos); // ⭐ 用选择器订阅
const dispatch = useDispatch();
return (
<ul>
{todos.map((t) => (
<li key={t.id} onClick={() => dispatch(toggleTodo(t.id))}>
{t.done ? '✅' : '⬜'} {t.text}
</li>
))}
</ul>
);
}
七、选择器与 useSelector 的性能
// ❌ 选择器返回「新对象」→ 每次都认为状态变了 → 每次都重渲染
const { todos, filter } = useSelector((s) => ({ todos: s.todos, filter: s.filter }));
// ✅ 方式一:分开订阅
const todos = useSelector((s) => s.todos);
const filter = useSelector((s) => s.filter);
// ✅ 方式二:用 shallowEqual
import { shallowEqual } from 'react-redux';
const { todos, filter } = useSelector((s) => ({ todos: s.todos, filter: s.filter }), shallowEqual);
// ✅ 方式三:用记忆化选择器(createSelector)
const visibleTodos = useSelector(selectVisibleTodos);
// ⚠️ 不要在选择器里做「重计算」
const total = useSelector((s) => s.items.reduce((sum, i) => sum + i.price, 0)); // ❌ 每次渲染都算
// ✅ 用 createSelector 记忆化
const selectTotal = createSelector([(s) => s.items], (items) => items.reduce((sum, i) => sum + i.price, 0));
八、什么时候用 Redux
✅ 适合:
① 状态复杂且被多个「不相关」的组件共享
② 需要「可预测 + 可回放」的状态变化(DevTools 时间旅行对调试很有帮助)
③ 中大型项目、多团队协作,需要「统一的状态管理规范」
④ 需要复杂中间件(日志、持久化、埋点、异步编排)
⑤ 状态需要「跨路由/跨页面长期存活」
❌ 不适合(或者说有更好的选择):
① 服务端数据(用 TanStack Query)
② 表单临时状态(用 react-hook-form)
③ 只有一两个组件共享的状态(props / Context)
④ 小项目(Zustand 更简单)
⑤ 需要「选择器订阅 + 极简 API」(Zustand 更合适)
九、常见坑
// ① reducer 里直接改 state
state.todos.push(action.payload); // ❌(除非用 Immer)
// ② 忘记 default 分支返回 state
// 未识别的 action 会返回 undefined → 状态被清空
// ③ 在 reducer 里做副作用(请求、日志、随机数)
// ④ 在 action 里放「函数」或「Promise」(普通 Redux 不支持)
dispatch(() => {}); // ❌ 需要 thunk 中间件
// ⑤ 把所有状态放进一个巨型 reducer(不按领域拆分)
// ⑥ 不写 action type 常量、到处写字符串(易拼错)
// ⑦ 用 useSelector 返回新对象(性能问题)
// ⑧ 缓存了 getState() 的结果(它是快照,不会更新)
const state = store.getState(); // ⚠️ 之后 state 不会变
// ⑨ 忘记在组件卸载时取消订阅(老式 store.subscribe 手动订阅)
// ⑩ 用 Redux 管「服务端数据」→ 手写缓存/失效/重试
// ⑪ 认为「Redux 一定比 Context 快」
// → 恰恰相反:Context 的问题是没有选择器,React-Redux 的价值就在于提供了它
// ⑫ 在 action 里放「不可序列化的值」(如 Date、Map)→ devtools 会警告
// ✅ 用字符串时间戳或 ID,或在中间件里处理
十、面试延伸
- 「Redux 的三个核心概念是什么?」
① Store——保存整个应用状态树的仓库(提供getState/dispatch/subscribe);② Action——描述「发生了什么」的普通对象(必须有type);③ Reducer——(state, action) => newState的纯函数,负责计算新状态。整个流程是dispatch(action)→ reducer 计算 → 更新 store → 通知订阅者 → 组件重新渲染,形成单向数据流。 -
「Redux 的三个原则是什么?为什么这么设计?」
① 单一数据源——所有状态在一个 store 里,便于调试与序列化(也便于 SSR 时把状态注入 HTML);② State 只读——只能通过dispatch(action)改变,保证「状态变化集中可控、没有隐式修改」;③ 用纯函数修改——reducer 必须是纯函数,这样「相同输入 → 相同输出」,从而支持时间旅行调试与状态回放,也让 reducer 极易单测。 -
「为什么 reducer 必须是纯函数?」
三个原因:① 保证可预测——相同输入得到相同输出,DevTools 的「回放」(重新执行所有 action 得到相同状态)才成立;② 避免副作用重复执行——reducer 可能在 StrictMode 下被双调用、也可能被 DevTools 重放,如果里面有请求/日志就会重复;③ 支持引用比较——直接修改参数会让引用不变,Redux(与 React)就无法判断「状态变了」。这也是为什么必须不可变更新。 -
「什么是不可变更新?为什么必须这样?」
指「不修改原状态,而是创建新的状态对象」(对象用展开、数组用非破坏性方法)。必须这样做的原因是:① Redux 与 React 都靠引用比较判断状态是否变化,直接修改会让引用不变,从而不触发更新;② 不可变性是时间旅行调试的基础(每个状态快照都要独立);③ 与React.memo/useSelector的浅比较机制配合。深层嵌套手写很痛苦,所以实践中用 Immer(RTK 内置)。 -
「Redux 的完整数据流是怎样的?」
UI 事件 → dispatch(action) → 中间件链(可选) → rootReducer(state, action) → 新 state 存入 store → store 通知所有订阅者 → 组件通过 useSelector 拿到新值 → 重新渲染。关键点是「单向」——没有反向的数据流(组件不能直接改 store),因此「谁改了状态、改了什么」全部可追踪、可回放。 -
「
useSelector有什么性能陷阱?」
主要陷阱是「选择器返回新对象」:useSelector(s => ({ a: s.a, b: s.b }))每次调用都返回新对象,Redux 用Object.is比较后认为「变了」→ 每次都重渲染。解法:① 分开订阅(每个字段一次useSelector);② 传shallowEqual作为比较函数;③ 用createSelector(reselect) 做记忆化派生。这与 Zustand / Context 的「引用稳定」问题同源。 -
「什么时候该用 Redux?」
适合:① 状态复杂且被多个不相关组件共享;② 需要「可预测 + 可回放」的调试体验(DevTools);③ 中大型项目、多团队协作需要统一规范;④ 需要复杂中间件(日志、持久化、异步编排)。不适合:服务端数据(用 TanStack Query)、表单状态(用 react-hook-form)、只有一两个组件共享的状态(props/Context)、小项目(Zustand 更简单)。判断标准是「是否真的需要那套约束与工具链」。 -
「Reselect 是什么?为什么需要它?」
Reselect 是 Redux 生态的记忆化选择器库,createSelector(inputSelectors, resultFn)会「只有输入变化时才重新计算结果」。它的必要性来自两点:① 避免重复计算——派生数据(过滤/排序/聚合)如果在useSelector里现算,每次渲染都会执行;② 保持引用稳定——记忆化后,输入不变时返回同一个引用,从而避免「每次都是新数组导致重渲染」的问题。RTK 里可以用createSelector或createEntityAdapter的selectAll。
一句话速记
Redux 三概念:store(唯一状态树)+ action(描述发生了什么)+ reducer(纯函数算新状态);三原则:单一数据源、state 只读、用纯函数修改(因此支持时间旅行调试,且 reducer 易测);必须不可变更新(靠引用比较触发更新,深层用 Immer,RTK 已内置);
useSelector的坑是「返回新对象导致每次重渲染」(分开订阅 /shallowEqual/createSelector记忆化);服务端数据不要放 Redux(用 TanStack Query)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!