阅读视图

发现新文章,点击刷新页面。

前端向架构突围系列 - 状态数据设计 [8 - 3]:服务端状态与客户端状态的架构分离

写在前面

架构师的核心能力之一是分类。 如果你觉得状态管理很痛苦,通常是因为你试图用同一种工具处理两种截然不同的东西:

  1. 客户端状态 (Client State): 比如“侧边栏是否展开”、“当前的夜间模式”。它们是同步的、瞬间完成的、由前端完全控制。
  2. 服务端状态 (Server State): 比如“用户订单列表”。它们是异步的、可能失效的、由后端控制。

Redux 并不擅长管理 Server State。 真正专业的做法是:让 Redux 回归 UI,让 TanStack Query (React Query) 接管 API。

image.png


一、 为什么要把 API 赶出 Redux?

1.1 消失的“样板代码”

在传统的 Redux 处理 API 流程中,你需要写:

  • 一个 Constant 定义 FETCH_USER_REQUEST
  • 一个 Action Creator
  • 一个处理 Pending/Success/ErrorReducer
  • 一个 useEffect 来触发请求

而在 TanStack Query 中,这只需要一行代码:

const { data, isLoading } = useQuery({ queryKey: ['user'], queryFn: fetchUser });

1.2 缓存与失效:Redux 的盲区

Server State 最难的不是“获取”,而是**“维护”**。

  • 用户离开页面 5 分钟后回来,数据还是新的吗?
  • 两个组件同时请求同一个接口,会发两次请求吗?
  • 弱网环境下,请求失败了会自动重试吗? 如果要用 Redux 实现这些,你需要写几百行复杂的 Middleware。而这些,是 Server State 管理工具的标配

二、 架构模型:双层数据流

现代前端架构推荐采用 “双层分离” 模型:

2.1 外部层:服务端状态 (Server State)

  • 工具: TanStack Query (React Query) 或 SWR。
  • 职责: 缓存管理、自动预取、失效检查 (Stale-While-Revalidate)、请求去重。
  • 特点: 它是异步的。

2.2 内部层:客户端状态 (Client State)

  • 工具: Zustand, Pinia, Jotai 或简单的 React Context。
  • 职责: 管理纯粹的 UI 逻辑(开关、多语言、主题、临时草稿)。
  • 特点: 它是同步的。

三、 实战战术:从“手动挡”切换到“自动挡”

3.1 自动化的依赖追踪

想象一个场景:你修改了用户的头像,你需要更新所有显示头像的地方。

  • 旧模式 (Redux): 修改成功后,手动发起一个 updateUserAction 去修改 Redux 里的那个大对象。
  • 新模式 (Query): 只需要执行一次“失效(Invalidate)”。
// 当用户修改个人资料成功时
const mutation = useMutation({
  mutationFn: updateProfile,
  onSuccess: () => {
    // 告诉系统:['user'] 这个 key 下的数据脏了,请自动重新拉取
    queryClient.invalidateQueries({ queryKey: ['user'] })
  },
})

架构意义: 你的代码不再需要关心“数据怎么同步”,只需要关心“数据何时失效”。

3.2 乐观更新 (Optimistic Updates)

这是架构高级感的核心。当用户点赞时,我们不等后端返回,直接改 UI。

TanStack Query 允许你在 onMutate 中手动修改缓存副本,如果请求失败,它会自动回滚。这种复杂的逻辑如果写在 Redux 里,会让 Reducer 逻辑变得极度臃肿。


四、 选型决策:什么时候该用谁?

作为架构师,你需要给团队划清界限:

状态类型 典型例子 推荐工具 存储位置
API 数据 商品列表、用户信息 TanStack Query 专用 Cache 池
全局 UI 状态 登录 Token、全局主题 Zustand / Pinia 全局 Store
局部 UI 状态 某个弹窗的开关 useState 组件内部
复杂表单 多步骤注册表单 React Hook Form 专用 Form State

导出到 Google 表格


五、 总结:让 Redux 变“瘦”

通过把 API 逻辑剥离出去,你会发现你的 Redux(或者 Zustand)Store 瞬间缩水了 80% 。 剩下的代码变得极其纯粹:只有纯同步的 UI 逻辑。

这种**“分治”**带来的好处是巨大的:

  1. 心智负担降低: 你不再需要管理复杂的 loading 状态机。
  2. 性能提升: TanStack Query 的细粒度缓存比 Redux 的全量对比快得多。
  3. 开发效率: 团队成员可以更专注地编写业务逻辑,而不是在样板代码中挣扎。

结语:控制的艺术

我们已经成功地将 API 数据和 UI 状态分开了。 但还有一种状态最让架构师头疼:流程状态。 当你的业务逻辑包含“待支付 -> 支付中 -> 支付成功/失败 -> 申请退款 -> 已关闭”这种复杂的链路时,无论你用什么工具,代码里都会充满 if/else

这种逻辑该如何优雅地管理?

Next Step: 下一节,我们将引入一个在航天和游戏领域应用了几十年的数学模型。 我们将学习如何用“图”的思想,终结代码里的逻辑乱麻。

前端向架构突围系列 - 状态数据设计 [8 - 2]:前端框架的“细粒度响应式”原理

写在前面

React 的痛: 在 React 中,一个 State 变了,组件就会重新执行(Re-render)。为了性能,我们不得不搞出 Fiber 架构,搞出时间切片,搞出 useMemo。这就好比:为了能在干草堆里找到一根针,React 发明了一台超级高科技的“干草堆翻找机”。

Signal 的解: 细粒度响应式(Signal)的思路是:在扔针进去的时候,就给针系上一根绳子。要找针的时候,拉绳子就行了。

本篇我们将深入内核,手写一个迷你 Signal 系统,看清它的本质。

image.png


一、 宏观对决:VDOM vs. Fine-Grained (细粒度)

要理解 Signal,首先要理解它想革谁的命。

1.1 VDOM 的“地毯式搜索”

React 的更新模型是 Snapshot(快照) 式的。

  • 流程: 数据变了 -> 运行整个组件函数 -> 生成新的 VDOM 树 -> 对比新旧树 (Diff) -> 找出差异 -> 更新 DOM。
  • 复杂度: 跟组件树的大小成正比。
  • 问题: 哪怕只改了一个文本节点,整个组件(甚至子组件)的逻辑都要重跑一遍。

1.2 Signal 的“点对点狙击”

SolidJS 或 Vue 的更新模型是 Dependency Graph(依赖图) 式的。

  • 流程: 数据变了 -> 直接定位到绑定了该数据的 DOM 节点 -> 更新 DOM。
  • 复杂度: 跟动态节点的数量成正比(通常是 O(1))。
  • 核心: 组件函数只在初始化时运行一次!之后再也不会运行了。

二、 解剖 Signal:发布订阅的进化体

Signal 并不神秘,它本质上就是 “保存值的容器” + “自动依赖追踪” 。 它由两个核心动作组成:Track (追踪/读)Trigger (触发/写)

2.1 核心 API 模拟

以 SolidJS/React 风格为例,我们造一个 Signal:

// 这是一个全局变量,用来记录“当前谁在查我不?”
let activeEffect = null;

function createSignal(initialValue) {
  let value = initialValue;
  const subscribers = new Set(); // 订阅者名单

  // Getter (读)
  const read = () => {
    if (activeEffect) {
      // 1. 依赖收集 (Track):如果有人在关注我,把他记下来
      subscribers.add(activeEffect);
    }
    return value;
  };

  // Setter (写)
  const write = (newValue) => {
    value = newValue;
    // 2. 派发更新 (Trigger):通知名单里所有人干活
    subscribers.forEach(fn => fn());
  };

  return [read, write];
}

2.2 魔法的粘合剂:Effect

光有 Signal 没用,得有人“读”它,订阅关系才能建立。这就需要 createEffect(在 Vue 里叫 watchEffect)。

function createEffect(fn) {
  // 把自己标记为“正在执行的副作用”
  activeEffect = fn;
  
  // 执行一次函数。
  // 注意:函数内部会读取 Signal,从而触发 Signal 的 Getter,
  // 进而把这个 fn 添加到 subscribers 里。
  fn();
  
  // 执行完复原
  activeEffect = null;
}

2.3 跑起来看看

const [count, setCount] = createSignal(0);

createEffect(() => {
  console.log("数字变了:", count()); 
});
// 输出: 数字变了:0 (初始化执行)

setCount(1);
// 输出: 数字变了:1 (自动触发!)

这就是细粒度响应式的最简内核。没有任何 VDOM,没有 Diff,只有精准的函数调用链。


三、 进阶: computed 与依赖图的自动构建

Signal 系统最强大的地方在于它能自动构建依赖图。 在架构设计中,我们经常使用 computed (派生状态)。

computed 既是 消费者(它依赖别的 Signal),又是 生产者(别的 Effect 依赖它)。

3.1 懒计算与缓存 (Memoization)

细粒度框架中的 computed 通常是惰性的(Lazy)。

  • 只有当有人读它时,它才计算。
  • 如果它依赖的 Signal 没变,它直接返回缓存。

3.2 动态依赖收集

这是 React useMemo 永远做不到的。 React 的依赖数组 [a, b] 是手动声明的(静态)。而 Signal 的依赖是运行时动态收集的。

const [show, setShow] = createSignal(true);
const [name, setName] = createSignal("Gemini");
const [age, setAge] = createSignal(18);

createEffect(() => {
  // 动态依赖!
  if (show()) {
    console.log(name()); // 此时依赖是 [show, name]
  } else {
    console.log(age());  // 此时依赖变成 [show, age]
  }
});

架构意义: 这种机制保证了最小化计算。当 show 为 false 时,改变 name 根本不会触发这个 Effect,因为系统知道这一刻 name 不重要。


四、 为什么 React 还在坚持?

既然 Signal 这么好,性能这么高,为什么 React 不把 useState 换成 Signal? 这涉及到底层哲学的冲突。

4.1 UI = f(state) vs. UI = Bind(state)

  • React 哲学: UI 是数据的投影(Snapshot) 。每次渲染都是丢弃旧世界,重建新世界。这符合函数式编程的直觉,心智模型最简单。
  • Signal 哲学: UI 是数据的绑定(Binding) 。初始渲染后,组件就消失了,剩下的只有数据和 DOM 之间的连线。

4.2 代数效应 (Algebraic Effects)

React 团队认为,手动处理 .value 或者 [get, set] 是对开发者心智的负担。他们追求的是 "It just works" 。 React 正在搞的 React Compiler (React Forget) ,其实是一条殊途同归的路:

  • Signal:运行时通过 Proxy 收集依赖,实现细粒度更新。
  • React Compiler:编译时分析代码,自动插入 memoization,模拟细粒度更新的效果。

五、 总结:架构师的选择

理解了原理,我们在架构设计中就能明白:

  1. Vue 3 / Solid: 适合高性能仪表盘、即时通讯、即时编辑类应用。因为它们对 CPU 的利用率极高,没有 VDOM 的 Overhead。

  2. React: 适合大型业务系统、生态依赖重的应用。虽然有一些性能损耗,但其编程模型的一致性(Pure Render)能降低逻辑复杂度。

  3. 趋势: 越来越多的状态管理库(MobX, Valtio, Preact Signals)允许你在 React 中使用 Signal。

    • 架构模式: 使用 Signal 管理频繁变化的局部状态(避免 React 顶层重渲染),使用 React Context 管理低频的全局状态

Next Step: 我们搞懂了前端“怎么存数据”(Redux/Atomic)和“怎么更新数据”(Signal)。 但还有一个最大的麻烦没解决:API 数据。 我们以前总是把后端返回的 JSON 也塞进 Redux 里,导致 Redux 变得臃肿不堪。这真的是对的吗? 下一节,我们将通过 React Query (TanStack Query) 来一场架构大扫除。 请看**《第三篇:分治——把 API 赶出 Redux:服务端状态 (Server State) 与客户端状态的架构分离》**。

❌