陌上人如玉
公子世无双

3.7 Redux 核心概念

3.7 Redux 核心概念

一句话: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,或在中间件里处理

十、面试延伸

  1. 「Redux 的三个核心概念是什么?」
    ① Store——保存整个应用状态树的仓库(提供 getState/dispatch/subscribe);② Action——描述「发生了什么」的普通对象(必须有 type);③ Reducer——(state, action) => newState 的纯函数,负责计算新状态。整个流程是 dispatch(action) → reducer 计算 → 更新 store → 通知订阅者 → 组件重新渲染,形成单向数据流。

  2. 「Redux 的三个原则是什么?为什么这么设计?」
    ① 单一数据源——所有状态在一个 store 里,便于调试与序列化(也便于 SSR 时把状态注入 HTML);② State 只读——只能通过 dispatch(action) 改变,保证「状态变化集中可控、没有隐式修改」;③ 用纯函数修改——reducer 必须是纯函数,这样「相同输入 → 相同输出」,从而支持时间旅行调试与状态回放,也让 reducer 极易单测。

  3. 「为什么 reducer 必须是纯函数?」
    三个原因:① 保证可预测——相同输入得到相同输出,DevTools 的「回放」(重新执行所有 action 得到相同状态)才成立;② 避免副作用重复执行——reducer 可能在 StrictMode 下被双调用、也可能被 DevTools 重放,如果里面有请求/日志就会重复;③ 支持引用比较——直接修改参数会让引用不变,Redux(与 React)就无法判断「状态变了」。这也是为什么必须不可变更新。

  4. 「什么是不可变更新?为什么必须这样?」
    指「不修改原状态,而是创建新的状态对象」(对象用展开、数组用非破坏性方法)。必须这样做的原因是:① Redux 与 React 都靠引用比较判断状态是否变化,直接修改会让引用不变,从而不触发更新;② 不可变性是时间旅行调试的基础(每个状态快照都要独立);③ 与 React.memo/useSelector 的浅比较机制配合。深层嵌套手写很痛苦,所以实践中用 Immer(RTK 内置)。

  5. 「Redux 的完整数据流是怎样的?」
    UI 事件 → dispatch(action) → 中间件链(可选) → rootReducer(state, action) → 新 state 存入 store → store 通知所有订阅者 → 组件通过 useSelector 拿到新值 → 重新渲染。关键点是「单向」——没有反向的数据流(组件不能直接改 store),因此「谁改了状态、改了什么」全部可追踪、可回放。

  6. 「useSelector 有什么性能陷阱?」
    主要陷阱是「选择器返回新对象」:useSelector(s => ({ a: s.a, b: s.b })) 每次调用都返回新对象,Redux 用 Object.is 比较后认为「变了」→ 每次都重渲染。解法:① 分开订阅(每个字段一次 useSelector);② 传 shallowEqual 作为比较函数;③ 用 createSelector(reselect) 做记忆化派生。这与 Zustand / Context 的「引用稳定」问题同源。

  7. 「什么时候该用 Redux?」
    适合:① 状态复杂且被多个不相关组件共享;② 需要「可预测 + 可回放」的调试体验(DevTools);③ 中大型项目、多团队协作需要统一规范;④ 需要复杂中间件(日志、持久化、异步编排)。不适合:服务端数据(用 TanStack Query)、表单状态(用 react-hook-form)、只有一两个组件共享的状态(props/Context)、小项目(Zustand 更简单)。判断标准是「是否真的需要那套约束与工具链」。

  8. 「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)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 3.7 Redux 核心概念

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫