全局状态管理:Flux架构与Redux

从一个真实的痛点说起

你有没有遇到过这种场景?一个数据在十几个组件间传来传去,props 层层嵌套,改一处逻辑得翻遍整个组件树。我之前接手过一个后台项目,十几个表单共享一份用户配置,每次修改都得顺着 props 链往上找源头,调试到崩溃。

说白了,这就是典型的 props drilling 问题。组件间通信一旦复杂起来,传统的事件总线或者父子传参根本扛不住。

核心矛盾:组件树越深,跨模块通信的链路就越脆弱。改一个叶子组件的数据,可能要波及整棵树的中间节点。

Flux 架构:单向数据流的启蒙

Facebook 当年在开发 Messenger 时也踩过同样的坑。他们的解决方案就是 Flux 架构。我第一次看到 Flux 的流程图时,第一反应是:「就这?绕这么大一圈?」后来真用上了才明白——单向循环才是最稳的。

Flux 的四层结构

  • View:只管渲染和触发用户操作,不存业务状态。
  • Action:描述「发生了什么」,是一个普通对象,带 type 和 payload。
  • Dispatcher:中枢调度器,负责把 Action 派发给所有注册的 Store。
  • Store:持有状态和业务逻辑,View 直接订阅它。

数据只能沿着 View → Action → Dispatcher → Store → View 这个方向转圈。你想想看,这样是不是每个环节都是可预测的?出 bug 也能顺着链条往回查。

我个人习惯把 Flux 的调度机制类比成「广播电台」—— Dispatcher 就是发射塔,所有 Store 都是收音机。同一套信号,谁收谁处理,互不干扰。

Redux:Flux 的最佳实践与进化

Flux 虽然思路清晰,但落地时有个麻烦:每个项目都得自己写 Dispatcher 和 Store 的模板代码。我不止一次在团队里抱怨过:「能不能有个现成的轮子?」Redux 就是那个答案。

Redux 在 Flux 基础上做了三个关键简化:

  1. 单一 Store:整个应用只有一棵状态树,调试和序列化都方便。
  2. Reducer 纯函数:状态更新逻辑全部收敛到纯函数里,输入输出可预测。
  3. 中间件机制:副作用(异步请求、日志)通过中间件链处理,不污染核心流程。

我曾经踩过的坑:Reducer 里不要直接修改 state。有一次我图省事写了 state.items.push(newItem),结果时间旅行调试直接废了。血的教训——始终返回新对象。

一个最小化的 Redux 示例

// Action
const addTodo = (text) => ({
  type: 'ADD_TODO',
  payload: { text, id: Date.now() }
});

// Reducer
const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { ...action.payload, completed: false }];
    default:
      return state;
  }
};

// Store
import { createStore } from 'redux';
const store = createStore(todos);

store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('学习 Flux 架构'));

注意看,Reducer 里用了扩展运算符 [...state, ...],而不是 push。这就是不可变数据的写法。我每次 code review 都会盯着这一行看。

中间件:Redux 的「插件系统」

Flux 没解决异步问题,Redux 用中间件优雅地补上了。中间件本质上是对 dispatch 的层层包装。你发一个 Action,它先过日志中间件,再过异步中间件,最后才到 Reducer。

我个人最常用的是 redux-thunk,简单直接:

const fetchUser = (id) => async (dispatch) => {
  dispatch({ type: 'FETCH_USER_START' });
  try {
    const user = await api.getUser(id);
    dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (err) {
    dispatch({ type: 'FETCH_USER_ERROR', payload: err.message });
  }
};

中间件的执行顺序就像洋葱圈——从外到内,再从内到外。这也是 Redux 最让我喜欢的设计之一。

性能优化:别让全局状态拖垮渲染

全局状态管理虽然方便,但用不好就是性能灾难。我见过一个项目,所有数据全塞在 Store 里,每次 dispatch 都要重新计算整个视图——卡得不行。

这里几个实用的优化手段:

手段 说明 我遇到的典型场景
reselect 选择器 带缓存的计算属性,只有依赖变化时才重新计算 列表过滤 + 排序,避免每次渲染都重算
浅比较绑定 connect 或 useSelector 使用浅比较,减少无效更新 大型表单,只更新变化字段对应的组件
拆分 Store 用 combineReducers 按模块拆分,粒度更细 用户模块、订单模块独立管理
不可变数据 配合 Immer 或手动不可变操作,保证引用比较准确 深层嵌套的数据结构更新

reselect 的缓存机制说白了就是「懒计算」——没人读取就不算,数据没变就复用上次结果。我一般用它来包装所有派生数据,性价比极高。

Flux vs Redux 对比一览

维度 Flux Redux
Store 数量 多个 单一
状态更新方式 Store 自包含逻辑 纯函数 Reducer
调度器 独立 Dispatcher 内置在 Store 中
副作用处理 无标准方案 中间件机制
调试体验 需手动实现 时间旅行、DevTools 开箱即用

我个人看法:Flux 适合理解设计思想,Redux 适合直接上手干活。如果团队规模不大、业务复杂度中等,直接用 Redux 更省心。

本章知识体系总览

下面这张图把 Flux 和 Redux 的核心流程串在一起了。你看一眼就能明白数据是怎么转圈的。

Flux / Redux 单向数据流架构 Flux / Redux 单向数据流架构 Flux 核心四层 View (用户界面) Action ({ type, payload }) Dispatcher (广播调度) Store (状态) 订阅更新 Redux 扩展:单一 Store + Reducer + 中间件 Reducer (纯函数) Middleware 链 (洋葱模型) 单一 Store Tree (可序列化) dispatch 拦截 connect / useSelector 💡 箭头方向 = 数据流方向。Redux 用中间件扩展了 Flux 的 dispatch 链路,但核心闭环不变。

到底选 Flux 还是 Redux?

我个人建议:

  • 小项目或概念验证:用手写 Flux 理解原理就够了,别上 Redux 全家桶。
  • 中大型业务系统:直接上 Redux + Toolkit。模板代码少,中间件生态成熟。
  • 已有旧项目用 Flux:不必强迁。如果状态逻辑不复杂,Flux 也能跑得很好。

记住一句话:状态管理是手段,不是目的。别为了用 Redux 而用 Redux。我见过不少项目,明明用 React Context 就能搞定,硬上 Redux 反而增加了复杂度。

本章小结

  • Flux 用单向数据流解决了跨模块通信的混乱问题。
  • Redux 在 Flux 基础上做了减法——单一 Store、纯函数 Reducer、中间件链。
  • 性能优化的三个关键词:选择器缓存、浅比较、不可变数据。
  • 中间件是 Redux 的精华,合理使用可以让副作用逻辑清晰可控。

嗯,这一章的内容就到这儿。单向数据流的思路其实不只在 Redux 里用,你写组件、设计接口、甚至规划业务流程时都能借鉴。核心就是——让数据朝着一个方向走,别绕圈子。