理解 Zustand 的工作原理(结合闭包)

profile image

通过剖析 Zustand 的源码,深入理解由闭包驱动的状态管理核心原理,揭开这个仅用约 100 行实现的强大状态管理库的秘密。

本帖由 Jetbrains's Coding Agent Junie junie logo翻译。如有任何翻译错误,请告知我们!

在学习闭包与作用域时,我发现 Zustand 是一个观察“如何利用闭包”的极佳真实案例,因此决定亲自深入阅读它的源码

核心结构 - vanilla.ts

Zustand 的精髓几乎都在 vanilla.ts 这约 100 行代码里。其余部分大多是为 React 和中间件做的“包装”。

为便于阅读,下文移除了类型。

javascript
// vanilla.ts
const createStoreImpl = (createState) => {
  // --- 闭包开始 ---
  let state;
  const listeners = new Set();

  const getState = () => state;

  const setState = (partial, replace) => {
    // partial 可能是函数,也可能是对象
    const nextState = typeof partial === 'function' ? partial(state) : partial;

    // 仅当前后状态不同时才更新并通知
    if (!Object.is(nextState, state)) {
      const previousState = state;
      // 用新状态替换(对象则合并,否则直接覆盖)
      state =
        replace ?? (typeof nextState !== 'object' || nextState === null)
          ? nextState
          : Object.assign({}, state, nextState);

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

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

  const destroy = () => listeners.clear();

  // 对外暴露的 API 对象
  const api = { setState, getState, subscribe, destroy };

  // 初始化!执行 createState 以设置初始状态
  state = createState(setState, getState, api);

  return api;
  // --- 闭包结束 ---
};

export const createStore = (createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl;

createStoreImpl 运行时,其中声明的 statelisteners 不会像普通局部变量那样消失——由于闭包,它们会一直保存在内存中。

返回的 api 对象中的 getStatesetStatesubscribe 等函数会记住它们创建时的环境(作用域),其中就包含 statelisteners

  • 封装:state 变量被“关”在 createStoreImpl 的作用域里。外部只能通过 getStatesetState 等 API 方法访问它。
  • 状态持久:createStoreImpl 虽只调用一次,但由此产生的 statelisteners 会在应用运行期间始终存在,因为 API 方法持续引用它们。

与 React 的集成 - react.ts

接下来看看如何将 vanilla store 与 React 集成。同样去掉了类型。

javascript
import React from 'react';
import { createStore } from './vanilla.js';

// 将 vanilla store 连接到 React 的桥接 hook
export function useStore(api, selector) {
  const slice = React.useSyncExternalStore(
    api.subscribe,
    () => selector(api.getState()),
    () => selector(api.getInitialState())
  );
  return slice;
}

const createImpl = (createState) => {
  // 1. 创建 vanilla store 的 API(state 与 listeners 在此通过闭包被创建)
  const api = createStore(createState);

  // 2. 创建一个与该 store 绑定的专用 Hook
  const useBoundStore = (selector) => useStore(api, selector);

  // 3. 将 API 方法(如 setState)挂到 Hook 本身上
  Object.assign(useBoundStore, api);

  return useBoundStore;
};

export const create = (createState) =>
  createState ? createImpl(createState) : createImpl;
  1. 创建 vanilla store。
  2. 通过 useBoundStore 构建一个返回 useSyncExternalStore 的 Hook。
  3. setStategetState 等 API 方法绑定到返回的 Hook 上。

useSyncExternalStore 的作用

useSyncExternalStore 是 React 18 引入的 Hook,用于让 React 与外部存储同步。它让 React 订阅外部数据源(此处为 Zustand 的 store),在变化发生时触发重新渲染。

javascript
// useSyncExternalStore 内部(简化版)
useEffect(() => {
  if (checkIfSnapshotChanged(inst)) {
    forceUpdate({inst});
  }
  const handleStoreChange = () => {
    if (checkIfSnapshotChanged(inst)) {
      // 强制重新渲染
      forceUpdate({inst});
    }
  };
  // 订阅 store 并返回清理函数
  return subscribe(handleStoreChange);
}, [subscribe]);

useSyncExternalStore 中,会将一个名为 handleStoreChange 的函数(用于比较快照并强制更新组件)注册为监听器。这样,当 setState 调用监听器时,handleStoreChange 就会执行,从而触发组件的重新渲染。

在开发者工具中查看

javascript
const useBearStore = create((set, get) => ({
  bears: 0,
  increase: () => set(state => ({ bears: state.bears + 1 })),
}));

function Component() {
  const bears = useBearStore(state => state.bears);

  return <h1>{bears} bears</h1>;
}

如果实际运行上面的组件,并在开发者工具的 Sources → Scope 选项卡中查看,可以看到所有内容都被闭包捕获,类似如下:

scope-dev-tools.webp

分步骤看运行流程

javascript
// 1. 创建 store Hook
const useBearStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
}));

// 2. 在组件中使用 store
function BearCounter() {
  const bears = useBearStore((state) => state.bears);
  return <h1>{bears} bears</h1>;
}

// 3. 更新状态
function Controls() {
  const increase = useBearStore((state) => state.increase);
  return <button onClick={increase}>one up</button>;
}

步骤 1:创建 Store(create

  1. 调用 create 时,createStoreImpl 被执行。
  2. 此时会在闭包作用域中为 useBearStore 创建独立的 statelisteners
  3. state 初始化为 { bears: 0, increase: [Function] }increase 通过闭包记住了 setState
  4. 返回名为 useBearStore 的自定义 Hook。该 Hook 同时携带 setStategetState 等方法。

步骤 2:组件渲染与订阅(useBearStore

  1. BearCounter 组件首次渲染时,useBearStore Hook 运行。
  2. 调用 useSyncExternalStore,并通过 subscribe 在步骤 1 创建的 listeners 中注册一个“触发重新渲染”的函数。
  3. 执行 getSnapshot 回调 () => state.bears,得到初始值 0,赋给 bears 变量并渲染出“0 bears”。

步骤 3:状态更新(increase

  1. 用户点击按钮时,在 Controls 组件中获取到的 increase 被执行。
  2. increase 调用其通过闭包记住的 setState
  3. setStatestate 更新为 { bears: 1 }
  4. setState 遍历 listeners 并调用所有已注册函数。
  5. BearCounter 中由 useSyncExternalStore 注册的“重新渲染函数”被调用。
  6. React 重新渲染 BearCounter,再次运行 getSnapshot,得到新值 1 并更新界面。

收尾

Zustand 是展示闭包力量与实用性的完美例子。仅用约 100 行代码(去掉类型约 40 行)就实现了以下目标:

  1. 封装:保护状态,避免被外部直接访问。
  2. 订阅模式:通过 SetuseSyncExternalStore 高效传播状态变化,只精确地重新渲染需要的组件。
  3. 简单:无需复杂的 reducer、action 类型或 dispatcher,提供直观的 API。

通过对 Zustand 的分析,我意识到像“闭包”这样不易直观把握的概念,一旦运用得当,就能构建强大的抽象。理解这些模式,有助于打造更好的状态管理方案,或更深入地理解现有库。