在学习闭包与作用域时,我发现 Zustand 是一个观察“如何利用闭包”的极佳真实案例,因此决定亲自深入阅读它的源码。
核心结构 - vanilla.ts
Zustand 的精髓几乎都在 vanilla.ts 这约 100 行代码里。其余部分大多是为 React 和中间件做的“包装”。
为便于阅读,下文移除了类型。
// 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
运行时,其中声明的 state
与 listeners
不会像普通局部变量那样消失——由于闭包,它们会一直保存在内存中。
返回的 api
对象中的 getState
、setState
、subscribe
等函数会记住它们创建时的环境(作用域),其中就包含 state
和 listeners
。
- 封装:
state
变量被“关”在createStoreImpl
的作用域里。外部只能通过getState
、setState
等 API 方法访问它。- 状态持久:
createStoreImpl
虽只调用一次,但由此产生的state
与listeners
会在应用运行期间始终存在,因为 API 方法持续引用它们。
与 React 的集成 - react.ts
接下来看看如何将 vanilla store 与 React 集成。同样去掉了类型。
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;
- 创建 vanilla store。
- 通过
useBoundStore
构建一个返回useSyncExternalStore
的 Hook。 - 将
setState
、getState
等 API 方法绑定到返回的 Hook 上。
useSyncExternalStore 的作用
useSyncExternalStore
是 React 18 引入的 Hook,用于让 React 与外部存储同步。它让 React 订阅外部数据源(此处为 Zustand 的 store),在变化发生时触发重新渲染。
// useSyncExternalStore 内部(简化版)
useEffect(() => {
if (checkIfSnapshotChanged(inst)) {
forceUpdate({inst});
}
const handleStoreChange = () => {
if (checkIfSnapshotChanged(inst)) {
// 强制重新渲染
forceUpdate({inst});
}
};
// 订阅 store 并返回清理函数
return subscribe(handleStoreChange);
}, [subscribe]);
在 useSyncExternalStore
中,会将一个名为 handleStoreChange
的函数(用于比较快照并强制更新组件)注册为监听器。这样,当 setState
调用监听器时,handleStoreChange
就会执行,从而触发组件的重新渲染。
在开发者工具中查看
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 选项卡中查看,可以看到所有内容都被闭包捕获,类似如下:
分步骤看运行流程
// 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
)
- 调用
create
时,createStoreImpl
被执行。 - 此时会在闭包作用域中为
useBearStore
创建独立的state
与listeners
。 state
初始化为{ bears: 0, increase: [Function] }
。increase
通过闭包记住了setState
。- 返回名为
useBearStore
的自定义 Hook。该 Hook 同时携带setState
、getState
等方法。
步骤 2:组件渲染与订阅(useBearStore
)
- 当
BearCounter
组件首次渲染时,useBearStore
Hook 运行。 - 调用
useSyncExternalStore
,并通过subscribe
在步骤 1 创建的listeners
中注册一个“触发重新渲染”的函数。 - 执行
getSnapshot
回调() => state.bears
,得到初始值0
,赋给bears
变量并渲染出“0 bears”。
步骤 3:状态更新(increase
)
- 用户点击按钮时,在
Controls
组件中获取到的increase
被执行。 increase
调用其通过闭包记住的setState
。setState
将state
更新为{ bears: 1 }
。setState
遍历listeners
并调用所有已注册函数。BearCounter
中由useSyncExternalStore
注册的“重新渲染函数”被调用。- React 重新渲染
BearCounter
,再次运行getSnapshot
,得到新值1
并更新界面。
收尾
Zustand 是展示闭包力量与实用性的完美例子。仅用约 100 行代码(去掉类型约 40 行)就实现了以下目标:
- 封装:保护状态,避免被外部直接访问。
- 订阅模式:通过
Set
与useSyncExternalStore
高效传播状态变化,只精确地重新渲染需要的组件。 - 简单:无需复杂的 reducer、action 类型或 dispatcher,提供直观的 API。
通过对 Zustand 的分析,我意识到像“闭包”这样不易直观把握的概念,一旦运用得当,就能构建强大的抽象。理解这些模式,有助于打造更好的状态管理方案,或更深入地理解现有库。