Zustand
从三个层次拆解 Zustand:它是什么、核心机制怎么运作、为什么能这么简洁。
一、Zustand 的本质
Zustand 解决的根本问题是:让任意组件能读取同一份状态,且状态变化时只重渲染真正关心它的组件。
它的实现极其精简——核心源码不到 100 行。整个库建立在两个原语上:
- 发布订阅模式(pub/sub)管理状态和监听器
useSyncExternalStore把外部 store 接入 React 的渲染循环
二、核心机制图解
先看整体结构:一个 store 在 React 树外部独立存在,组件通过 hook 订阅它。
三、手写一个最小 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 是关键:
useSyncExternalStore 在每次 store 通知后重新调用 selector(getState()),用 Object.is 比较新旧结果。只要 selector 返回的值没变,React 直接 bailout,组件不重渲染。
这就是为什么 useStore(s => s.count) 在 name 变化时不会重渲染 ComponentA。
五、为什么不用 useState 或 useContext?
| 方案 | 问题 |
|---|---|
useState 提升 | 状态提升到公共祖先,任何变化导致整棵子树重渲染 |
useContext | 无法做 selector,context value 一变,所有消费者全部重渲染 |
useReducer + Context | 同上,仍然是 context 的全量通知问题 |
| Zustand | store 在 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保证最小重渲染 - 极简 API:
create+useStore,没有 action/reducer/dispatch 概念 - 框架无关的内核:vanilla store 可以在 React 之外独立使用