My App

Zustand

从三个层次拆解 Zustand:它是什么、核心机制怎么运作、为什么能这么简洁。

一、Zustand 的本质

Zustand 解决的根本问题是:让任意组件能读取同一份状态,且状态变化时只重渲染真正关心它的组件。

它的实现极其精简——核心源码不到 100 行。整个库建立在两个原语上:

  • 发布订阅模式(pub/sub)管理状态和监听器
  • useSyncExternalStore 把外部 store 接入 React 的渲染循环

二、核心机制图解

先看整体结构:一个 store 在 React 树外部独立存在,组件通过 hook 订阅它。

Zustand 整体架构

三、手写一个最小 Zustand

理解原理最好的方式是把它从零写出来。核心只有两层:

第一层:vanilla store(与 React 无关)

function createStore(initializer) {
  let state;
  const listeners = new Set();

  const setState = (partial) => {
    // 支持函数式更新和对象 merge
    const next = typeof partial === "function" ? partial(state) : partial;
    // 只在真正变化时才通知
    if (!Object.is(next, state)) {
      state = typeof next !== "object" ? next : Object.assign({}, state, next);
      listeners.forEach((listener) => listener(state));
    }
  };

  const getState = () => state;

  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener); // 返回取消订阅函数
  };

  // 初始化:调用 initializer,把 setState/getState 传给它
  state = initializer(setState, getState, { setState, getState, subscribe });

  return { setState, getState, subscribe };
}

这就是一个完整的 与框架无关的状态容器。它的能力:

  • getState() —— 随时拿到当前状态
  • setState(partial) —— 更新状态并通知所有监听者
  • subscribe(listener) —— 注册监听,返回取消函数

第二层:React hook(用 useSyncExternalStore 接入 React)

import { useSyncExternalStore } from "react";

function useStore(store, selector = (s) => s) {
  return useSyncExternalStore(
    store.subscribe, // 订阅函数
    () => selector(store.getState()) // 取快照
  );
}

就这两层,加起来不到 30 行,已经是完整可用的 Zustand。

完整使用示例

// 创建 store
const counterStore = createStore((set) => ({
  count: 0,
  name: "counter",
  increment: () => set((s) => ({ count: s.count + 1 })),
  setName: (name) => set({ name }),
}));

// 组件中使用
function ComponentA() {
  const count = useStore(counterStore, (s) => s.count);
  const increment = useStore(counterStore, (s) => s.increment);
  return <button onClick={increment}>{count}</button>;
}

function ComponentB() {
  const name = useStore(counterStore, (s) => s.name);
  return <span>{name}</span>;
}

四、精准重渲染的关键:selector

光有发布订阅还不够。每次 setState 会通知所有订阅组件,但我们希望只重渲染真正受影响的组件。selector 是关键

Zustand selector 精准重渲染流程

useSyncExternalStore 在每次 store 通知后重新调用 selector(getState()),用 Object.is 比较新旧结果。只要 selector 返回的值没变,React 直接 bailout,组件不重渲染。

这就是为什么 useStore(s => s.count)name 变化时不会重渲染 ComponentA。

五、为什么不用 useStateuseContext

方案问题
useState 提升状态提升到公共祖先,任何变化导致整棵子树重渲染
useContext无法做 selector,context value 一变,所有消费者全部重渲染
useReducer + Context同上,仍然是 context 的全量通知问题
Zustandstore 在 React 外部,selector + Object.is 实现精准订阅

useContext 的根本限制

// Context 方案:name 变了,所有消费者都重渲染,即使只用了 count
const MyContext = createContext();

function Parent() {
  const [state, setState] = useState({ count: 0, name: "" });
  return (
    <MyContext.Provider value={state}>
      <ComponentA /> {/* 只用 count,但 name 变了也会重渲染 */}
      <ComponentB /> {/* 只用 name,但 count 变了也会重渲染 */}
    </MyContext.Provider>
  );
}

React 的 Context 使用引用相等来判断是否需要重渲染。只要 Provider 的 value 引用变了,所有 useContext 的消费者都会重渲染,无论它们实际用了哪个字段。

六、交互演示

点击按钮观察 selector 如何决定哪些组件重渲染、哪些被跳过:

七、Zustand 的设计哲学总结

用一句话概括:Zustand 就是一个带"谁在听"列表的普通对象,加上一个能把这个对象接入 React 渲染循环的 hook。

它没有魔法——只是把发布订阅这个古老模式,用 useSyncExternalStore 精确地嫁接到了 React 的调度系统里,再用 selector 把"通知所有人"变成"只更新真正关心的人"。极简,但足够。

与 Redux 的本质区别

Redux 用 Context + Provider 传递 store,Zustand 用模块级变量(closure)持有 store——这是两者最根本的区别,也是 Zustand 不需要 <Provider> 的原因。

Redux:   <Provider store={store}> → Context → useSelector
Zustand: createStore() → 闭包持有 → useSyncExternalStore

核心优势

  • 零 Provider:store 是模块级闭包,不依赖 React 树结构
  • 精准更新:selector + Object.is 保证最小重渲染
  • 极简 APIcreate + useStore,没有 action/reducer/dispatch 概念
  • 框架无关的内核:vanilla store 可以在 React 之外独立使用

On this page