Skip to main content

Zustand 状态管理

Zustand(德语中的“状态”)是目前 React 社区最火的轻量级状态管理库之一。它主打轻量、极简、Hook 驱动。相比于 Redux 的繁杂和 MobX 的魔法,Zustand 在不可变数据(Immutable)和易用性之间找到了完美的平衡。

在 React 中的使用

Zustand 不需要像 Redux 那样在最外层包裹 <Provider>,它通过 create 函数直接创建一个自定义 Hook,在任何组件中都可以即插即用。

1. 创建 Store

// store/useBearStore.js
import { create } from "zustand";

// create 接收一个函数,该函数接收 set 和 get 方法,返回 store 的初始状态和操作方法
export const useBearStore = create((set, get) => ({
// State
bears: 0,

// Action:通过 set 方法更新状态
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),

removeAllBears: () => set({ bears: 0 }),

// 可以通过 get() 获取当前状态
logBears: () => {
console.log(`There are ${get().bears} bears`);
},

// 异步 Action 非常自然,直接写 async/await 即可,不需要 Thunk/Saga
fetchBears: async () => {
const response = await fetch("/api/bears");
const data = await response.json();
set({ bears: data.length });
},
}));

2. 在 React 组件中接入

通过选择器(Selector)提取所需的状态,Zustand 会自动处理组件的订阅和细粒度更新。

import React from "react";
import { useBearStore } from "./store/useBearStore";

function BearCounter() {
// 传入 selector:只订阅 bears 状态。bears 变化时组件重渲染
const bears = useBearStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}

function Controls() {
// 也可以只提取 action,这样状态变化时这个组件不会重渲染!
const increasePopulation = useBearStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}

底层原理剖析

面试官问:“Zustand 为什么不需要 Provider?它是怎么触发 React 组件更新的?”

1. 核心闭包与发布订阅模式(Pub/Sub)

Zustand 的核心非常精简(去掉类型定义不到百行代码)。它的本质是一个在模块作用域(Module Scope)内维护的闭包变量,配合发布订阅模式

为了便于面试理解,我们来看它的 vanilla(原生)实现伪代码:

// Zustand 极简底层伪代码 (Vanilla 侧)
const createStore = (createState) => {
let state;
const listeners = new Set();

const setState = (partial, replace) => {
// 支持传入函数或直接传对象
const nextState = typeof partial === "function" ? partial(state) : partial;
// 只有状态改变了才触发更新
if (!Object.is(nextState, state)) {
state =
(replace ?? typeof nextState !== "object")
? nextState
: Object.assign({}, state, nextState); // 浅合并

// 通知所有订阅者(即 React 组件)
listeners.forEach((listener) => listener(state, state));
}
};

const getState = () => state;

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

const api = { setState, getState, subscribe };
state = createState(setState, getState, api); // 初始化状态
return api;
};

因为状态是保存在外部的闭包变量 state 中,独立于 React 的组件树,所以完全不需要 Context Provider。这也是为什么它能在非 React 环境(如普通 JS 函数中)通过 useBearStore.getState() 直接获取状态的原因。

2. 细粒度更新机制与 React 结合

当你这样写时:const bears = useBearStore(state => state.bears); Zustand 做了两件事:

  1. 订阅:将当前组件注册到上面的 listeners 中。
  2. 状态比对:当状态更新触发 listener 回调时,Zustand 会用你传入的 selector 函数去获取新值(newState.bears),并与旧值进行严格相等(===)比对。
    • 如果相等,什么也不做,完美避免了无关组件的重渲染。
    • 如果不相等,则触发 React 组件的强制更新。

在 React 18 之后,Zustand 底层全面接入了 React 官方提供的 useSyncExternalStore API。我们来看看它的 Hook 是怎么包住 vanilla store 的:

// Zustand 极简 Hook 伪代码 (React 侧)
import { useSyncExternalStore } from "react";

export function useStore(api, selector) {
// 利用官方 API 将外部状态同步进 React 渲染周期
const slice = useSyncExternalStore(
api.subscribe,
() => selector(api.getState()), // 传入 selector 提取局部状态
);

return slice;
}
  • 解决页面撕裂(Tearing):在 React 18 的并发渲染(Concurrent Mode)下,如果组件读取的是外部可变状态,可能会导致一个渲染周期内不同组件读到的状态不一致(撕裂)。useSyncExternalStore 专门用于安全地订阅外部 Store,一旦外部状态改变,React 会中断当前并发渲染,进行同步更新,确保 UI 一致性。

4. 中间件机制

Zustand 的中间件设计非常巧妙,通过高阶函数的形式包装 setget。 例如,持久化中间件 persist 和集成 Redux DevTools 的中间件,都是拦截 set 函数,在更新内存状态的同时,将状态同步到 localStorage 或发送给 DevTools 插件。

面试话术提炼(与 Redux 的对比)

  • 心智负担:Zustand 抛弃了 Redux 的 Action Types、Reducers、Dispatch 等繁杂概念,直接将 State 和修改 State 的方法(Actions)合并写在一个 Hook 里,极其符合 React 开发者直觉。
  • 异步处理:不需要引入 Redux-Thunk 或 Redux-Saga,直接在 Action 中写普通的 async/await 即可。
  • 性能:基于 Selector 的细粒度控制,加上剥离 Context 的设计,彻底解决了 React Context 带来的全量重渲染灾难。
  • 适用场景:对于中小型项目甚至大部分没有极端复杂状态流转的大型项目,Zustand 都是目前的最佳选择;但如果是多人协作、状态流转需要极其严格的审计和回溯记录的超大型应用,Redux 的严格规范仍然有其价值。