- Published on
Zustand 源码解析
- Authors
- Name
- 青雲
引言
简介
Zustand 是什么
Zustand 是一个轻量级的状态管理库,其名字在德语中意为“状态”。相较于其他复杂的状态管理方案,Zustand 提供了一种更简单、更直观的方式来管理 React 应用的状态。它最初是由 Poimandres 团队开发的,该团队还开发了其他受欢迎的库如 react-spring(React动画库,提供了强大的物理基础动画功能)、react-three-fiber(针对three.js的React渲染器,以声明式方式创建和控制3D内容)等。Zustand 的核心理念是通过使用一个简单的 API 来简化状态管理,减少样板代码,使开发者能更专注于业务逻辑。
与其他状态管理库的对比
- Redux:
- Redux 是一个非常流行的状态管理库,提供了单一的全局状态树以及中间件机制。
- 优点: 强类型支持,生态系统丰富,调试工具完善。
- 缺点: 配置复杂,样板代码多,学习曲线陡峭。
- MobX:
- MobX 提供了响应式的状态管理,通过类和装饰器实现状态的自动同步。
- 优点: 易于使用,自动跟踪依赖,减少样板代码。
- 缺点: 学习曲线稍有难度,复杂项目中的性能问题。
- Recoil:
- Recoil 是 Facebook 开发的一个新的状态管理库,提供了基于 atoms 和 selectors 的机制来管理状态。
- 优点: 与 React 紧密集成,提供了很好的性能和灵活性。
- 缺点: 尚处于实验性阶段,生态系统较小。
- Jotai:
- Jotai 通过原子概念(atoms)来管理状态,允许你构建出高度模块化的状态管理方案。
- 优点: 精简API,更细粒度的状态切分和复用,支持并发模式,与 React 并发特性相适应。
- 缺点: 概念相对新颖,需要一定的学习成本,社区和生态不如其他状态库成熟。
- Zustand:
- Zustand 提供了一个非常简单和直接的 API 使用 create 函数来定义状态和操作。
- 优点: 非常轻量,无依赖,使用简单,支持中间件,React hooks 支持。
- 缺点: 功能相对较少,没有官方的调试工具。
总的来说,Zustand 是一个中介型的状态管理库,站在“简单易用”和“功能强大”之间的平衡点上。随着 React Hook 的广泛应用,Zustand 的使用体验与 React 更加契合,特别适合于中小型项目。
目的
解析 Zustand 的源码,理解其内部实现,能够帮助我们更深层次地了解其设计理念和工作机制。在使用过程中可以更加自如,而不仅仅停留在文档层面。通过对源码的研究,我们可以获得以下几个方面的收获:
- 掌握设计思想: 了解作者在设计 Zustand 时所考虑的问题,以及如何解决这些问题的。
- 优化和扩展能力: 在特定的场景下,能够定制和优化 Zustand 的性能。
- 提升技术深度: 深入理解一个状态管理库的实现,有助于提升自己在前端领域的技术深度。
接下来,我们将从基础环境的准备开始,一步步深入到 Zustand 的源码世界。
环境准备
在解析 Zustand 源码之前,我们需要进行一些前期的环境准备工作,以确保我们能顺利地阅读和调试源码。
所需工具
- Node.js 和 pnpm:用 Node.js 进行环境搭建,并使用 pnpm 下载相关依赖。
- Git:用 Git 来克隆 Zustand 的代码仓库。
- 代码编辑器:推荐使用 Visual Studio Code (VS Code) 或其他你熟悉的代码编辑器。
- 其他插件和工具:VS Code 插件:Prettier,ESLint,Path Intellisense 等。
克隆 Zustand 仓库
- 打开终端或命令提示符,导航到你想存放代码的位置。
- 执行以下命令将 Zustand 仓库克隆到本地:
git clone https://github.com/pmndrs/zustand.git
- 进入克隆好的项目目录:
cd zustand
- 安装项目依赖:
pnpm install
项目结构介绍
在我们开始解析源码之前,先来看一下 Zustand 项目的目录结构,这有助于我们快速定位和分析源码中的关键部分。
zustand/
├── docs/ # 文档目录
├── examples/ # 示例代码目录
├── src/ # 源代码目录
│ ├── middleware/
│ │ ├── combine.ts
│ │ ├── devtools.ts
│ │ ├── immer.ts
│ │ ├── persist.ts
│ │ ├── redux.ts
│ │ └── subscribeWithSelector.ts
│ ├── react/
│ │ └── shallow.ts
│ ├── vanilla/
│ │ └── shallow.ts
│ ├── context.ts
│ ├── index.ts
│ ├── middleware.ts
│ ├── react.ts
│ ├── shallow.ts
│ ├── traditional.ts
│ ├── types.d.ts
│ └── vanilla.ts
├── tests/ # 测试代码目录
├── package.json # 项目配置文件
├── README.md # 项目说明文档
└── ...
关键目录和文件说明
src/
:- middleware/:
combine.ts:
实现了将多个 store 组合在一起的中间件。devtools.ts
: 实现了与 Redux DevTools 进行交互的中间件。immer.ts
: 支持 immer 库的中间件,用于不可变数据结构。persist.ts
: 实现了持久化 store 的中间件。redux.ts
: 模拟 Redux 风格的中间件。subscribeWithSelector.ts
: 支持选择订阅和响应的中间件。
- react/:
shallow.ts
: 实现 react 相关的浅比较函数,用于优化组件渲染。
- vanilla/:
shallow.ts
: 实现原生状态管理版本的浅比较函数。
context.ts
: 实现 React Context 提供的支持。index.ts
: Zustand 的主要入口文件,会汇总和输出所有核心功能。middleware.ts
: 汇总和重导出中间件。react.ts
: 实现了 Zustand 与 React 的绑定,导出有关 React 接口和create
等 。shallow.ts
: 提供浅比较功能,通常用于比较状态变化以优化性能。traditional.ts
: 提供了传统的状态管理模式,适用于不使用 hooks 的场景。types.d.ts
: TypeScript 类型声明文件,定义了 Zustand 中使用的类型。vanilla.ts
: 实现了 Zustand 的核心功能,不依赖于 React。这使得 Zustand 可以在其他环境(如 React Native)中使用。
- middleware/:
架构图
核心概念解析
create
方法解析
源码位置
create
方法位于 src/react.ts
文件中,而它依赖的核心功能主要在 src/vanilla.ts
文件中实现。src/vanilla.ts
文件包括了 zustand 的核心功能,这些功能是框架无关的。src/react.ts
导出了适用于 React 的定制化 hooks,增强了 zustand 与 React 的集成,使得在 React 应用中使用 zustand 变得更加自然和高效。
这种设计遵循了模块化和关注点分离的原则,通过分离核心逻辑(vanilla)和特定于框架的逻辑(react),允许 Zustand 同时服务于 React 和非 React 项目,在灵活性、可扩展性和可维护性方面实现了良好的平衡。
功能和作用
create
方法的主要作用是初始化一个 Zustand store。它接受一个状态初始化函数,并返回一个包含状态管理方法的对象。其职责包括:
- 初始化状态。
- 提供 getState 和 setState 方法来读取和更新状态。
- 提供 subscribe 方法来订阅状态变化。
- 绑定 React 提供 useStore hook。
export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create
create
方法封装了 Zustand 的 store 创建和状态管理逻辑,使得使用者可以通过简单直接的方式来管理状态。
Store 的创建与初始化
内部实现
在 Zustand 中,store 的创建是通过 create
方法实现的。其中 createStoreImpl
函数用于完成状态管理逻辑的初始化,首先声明了一个状态对象 (state
)、一个监听器集合 (listeners
) 以及几个核心函数 (setState
、getState
、subscribe
等)。这些核心函数允许外部组件不仅可以获取当前的状态,还可以更新状态,并在状态更新时接收通知。
通过调用初始化函数 createState,用户定义的状态和更新逻辑会被绑定到这些核心函数上,从而完成了 store 的配置。
const createStoreImpl: CreateStoreImpl = (createState) => {
type TState = ReturnType<typeof createState>;
type Listener = (state: TState, prevState: TState) => void;
let state: TState;
const listeners: Set<Listener> = new Set();
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState =
typeof partial === 'function'
? (partial as (state: TState) => TState)(state)
: partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state =
replace ?? (typeof nextState !== 'object' || nextState === null)
? (nextState as TState)
: Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState: StoreApi<TState>['getState'] = () => state;
const getInitialState: StoreApi<TState>['getInitialState'] = () => initialState;
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener);
// Unsubscribe function
return () => listeners.delete(listener);
};
const destroy: StoreApi<TState>['destroy'] = () => {
console.warn(
'[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use the unsubscribe function returned by subscribe.'
);
listeners.clear();
};
const api = { setState, getState, getInitialState, subscribe, destroy };
const initialState = (state = createState(setState, getState, api));
return api as any;
};
初始状态管理
getInitialState
函数提供了一种机制,用于在 store 创建时获取初始状态。这是通过在 createState
调用中,利用用户提供的状态定义来实现的。它确保了无论何时创建 store,都可以获得一致的初始状态。
const getInitialState: StoreApi<TState>['getInitialState'] = () => initialState;
初始状态由 create
方法的参数传入,可以是一个状态对象,或者是一个接收 setState
、getState
和 store
的函数。通过这种方式,Zustand 提供了灵活的状态初始化方法。
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
在这个例子中,初始状态为 { count: 0 }
,并且定义了 increment
方法来更新状态。
状态订阅与更新机制
订阅的实现
通过 subscribe
函数,外部组件可以将一个监听器(listener)添加到内部的监听器集合中。每当状态更新时(通过 setState
),所有的监听器都会被调用,以通知状态的变化。
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener);
// 返回取消订阅的函数
return () => listeners.delete(listener);
};
监听器在注册时会返回一个取消订阅的函数,调用该函数可以从监听器集合中移除监听器。
状态的推送和变更流程
当调用 setState
函数更新状态时,会根据提供的新状态或更新函数计算出下一个状态。如果新状态与当前状态不同,则会将新状态设置为当前状态,并遍历调用所有注册的监听器,传递新状态和前一个状态作为参数。
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState =
typeof partial === 'function'
? (partial as (state: TState) => TState)(state)
: partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state =
replace ?? (typeof nextState !== 'object' || nextState === null)
? (nextState as TState)
: Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
在 setState
中,可以选择替换整个状态或部分更新。更新操作完成后,会遍历所有监听器并调用它们,通知状态发生了变化。
React 组件订阅
React 组件是通过 useStore
hook 内部订阅 store 来实现订阅和响应状态变化的。这是通过 useSyncExternalStoreWithSelector 实现的,该函数用于在状态变化时通知 React 组件进行重新渲染。
import ReactExports from 'react';
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
import { createStore } from './vanilla.ts';
import type { Mutate, StateCreator, StoreApi, StoreMutatorIdentifier } from './vanilla.ts';
const { useDebugValue } = ReactExports;
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
...
export function useStore<TState, StateSlice api: WithReact<ReadonlyStoreApi<TState>>,
selector: (state: TState) => StateSlice = identity as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
if (
import.meta.env?.MODE !== 'production' &&
equalityFn &&
!didWarnAboutEqualityFn
) {
console.warn(
"[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937",
);
didWarnAboutEqualityFn = true;
}
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getInitialState,
selector,
equalityFn,
);
useDebugValue(slice);
return slice;
}
useSyncExternalStoreWithSelector
:该函数是 React 官方提供的useSyncExternalStore
hook 的增强版本,添加了对选择器函数的支持。通过该函数,React 组件可以订阅外部 store 并在状态变化时重新渲染。- 订阅流程:当组件调用
useStore
hook 时,该 hook 内部会调用useSyncExternalStoreWithSelector
,并传入以下几个参数:- api.subscribe: 用于订阅 store 状态变化的函数。每当状态通过 setState 发生变化时,会调用所有已订阅的回调函数。
- api.getState: 用于获取当前 store 状态的函数。
- api.getInitialState: 用于获取 store 初始状态的函数。
- selector: 用于从状态中提取需要的 slice(片段)的函数。
- equalityFn: 用于比较两个状态是否相同的函数,如果两个状态相同则不会触发重渲染。
import create from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useStore = create(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'count-storage' }
)
)
);
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
<div>
<span>{count}</span>
<button onClick={increment}>Increment</button>
</div>
);
}
在这个示例中,当组件 Counter
渲染时,它通过 useStore
hook 订阅 count
状态的变化。每当 increment
函数被调用并更新状态后,状态发生变化,useSyncExternalStoreWithSelector
会使组件重新渲染,count
的最新值会被显示在页面上。
通过这种方式,React 组件可以在 Zustand 的 store 状态变化时自动更新,无需手动管理订阅和状态同步逻辑,非常方便。
中间件支持
如何添加中间件
Zustand 支持通过中间件对 create
方法进行增强。通过高阶函数可以在状态创建和更新的不同阶段插入自定义逻辑。
const log = (config) => (set, get, api) => config((args) => {
console.log('prev state', get());
set(args);
console.log('new state', get());
}, get, api);
const useStore = create(log((set) => ({
count: 1,
increment: () => set(state => ({ count: state.count + 1 })),
})));
在这个示例中,log
中间件在每次状态更新前后打印状态变化,起到了日志记录的作用。
常见中间件解析
combine
- 功能:将多个 store 组合在一起,适用于复杂状态管理。
combine
函数用来将两个状态对象组合成一个状态对象。它接受两个参数:初始状态initialState
和附加状态创建函数additionalStateCreator
。返回一个新的状态创建函数,这个新的状态创建函数会将初始状态和附加状态合并为一个状态对象。
export const combine = (initialState, create) => (...args) =>
Object.assign({}, initialState, create(...args));
在使用 combine
中间件时,初始状态作为基础状态,附加状态创建函数允许我们结合其它状态或方法。例如:
const useStore = create(
combine(
{ bears: 0 },
(set) => ({
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
})
)
);
devtools
- 功能:提供与 Redux DevTools 进行交互的功能,便于调试状态变化。
devtools
中间件允许开发者通过 Redux DevTools 工具监控状态变化和调试。核心实现是对setState
方法进行包装,并在每次状态更新时向 Redux DevTools 发送状态变化的通知。
let extensionConnector:
| (typeof window)['__REDUX_DEVTOOLS_EXTENSION__']
| false;
try {
extensionConnector =
(enabled ?? import.meta.env?.MODE !== 'production') &&
window.__REDUX_DEVTOOLS_EXTENSION__;
} catch (e) {
// ignored
}
这一部分代码尝试在浏览器中获取 Redux DevTools 扩展,如果未安装则会输出警告信息。
(api.setState as NamedSet<S>) = (state, replace, nameOrAction) => {
const r = set(state, replace);
if (!isRecording) return r;
const action: { type: string } =
nameOrAction === undefined
? { type: anonymousActionType || 'anonymous' }
: typeof nameOrAction === 'string'
? { type: nameOrAction }
: nameOrAction;
if (store === undefined) {
connection?.send(action, get());
return r;
}
connection?.send(
{
...action,
type: `${store}/${action.type}`,
},
{
...getTrackedConnectionState(options.name),
[store]: api.getState(),
}
);
return r;
};
包装了 setState
方法,以便在每次状态更新时,向 Redux DevTools 发出状态更新通知。这样可以在 DevTools 中查看状态的变化。
connection.subscribe((message: any) => {
switch (message.type) {
case 'ACTION':
if (typeof message.payload !== 'string') {
console.error(
'[zustand devtools middleware] Unsupported action format'
);
return;
}
return parseJsonThen<{ type: unknown; state?: PartialState }>(message.payload, (action) => {
if (action.type === '__setState') {
setStateFromDevtools(action.state as PartialState);
return;
}
if ((api as any).dispatchFromDevtools) {
(api as any).dispatch(action);
}
});
case 'DISPATCH':
switch (message.payload.type) {
case 'RESET':
setStateFromDevtools(initialState as S);
connection?.init(api.getState());
return;
case 'COMMIT':
connection?.init(api.getState());
return;
case 'ROLLBACK':
return parseJsonThen<S>(message.state, (state) => {
setStateFromDevtools(state);
connection?.init(api.getState());
});
case 'JUMP_TO_STATE':
case 'JUMP_TO_ACTION':
return parseJsonThen<S>(message.state, (state) => {
setStateFromDevtools(state);
});
case 'IMPORT_STATE': {
const { nextLiftedState } = message.payload;
const lastComputedState = nextLiftedState.computedStates.slice(-1)[0]?.state;
if (!lastComputedState) return;
setStateFromDevtools(lastComputedState);
connection?.send(null as any, nextLiftedState); // Notify DevTools about the new state
return;
}
case 'PAUSE_RECORDING':
isRecording = !isRecording;
return;
}
}
});
该代码片段订阅了 Redux DevTools 的消息,并相应地更新 Zustand 的状态。当 DevTools 发送 RESET
、COMMIT
、ROLLBACK
等命令时,对应的处理逻辑也会同步更新 Zustand 的状态。
const { connection, ...connectionInformation } =
extractConnectionInformation(store, extensionConnector, options);
const extractConnectionInformation = (
store: string | undefined,
extensionConnector: NonNullable<
(typeof window)['__REDUX_DEVTOOLS_EXTENSION__']
>,
options: Omit<DevtoolsOptions, 'enabled' | 'anonymousActionType' | 'store'>,
) => {
if (store === undefined) {
return {
type: 'untracked' as const,
connection: extensionConnector.connect(options),
};
}
const existingConnection = trackedConnections.get(options.name);
if (existingConnection) {
return { type: 'tracked' as const, store, ...existingConnection };
}
const newConnection: ConnectionInformation = {
connection: extensionConnector.connect(options),
stores: {},
};
trackedConnections.set(options.name, newConnection);
return { type: 'tracked' as const, store, ...newConnection };
};
通过 extractConnectionInformation
函数,检测当前 Redux DevTools 是否已经连接,若没有则创建新的连接。这一部分主要管理多个 store 和 DevTools 连接之间的关系。
const parseJsonThen = <T>(stringified: string, f: (parsed: T) => void) => {
let parsed: T | undefined;
try {
parsed = JSON.parse(stringified);
} catch (e) {
console.error(
'[zustand devtools middleware] Could not parse the received json',
e,
);
}
if (parsed !== undefined) f(parsed as T);
};
这是一个辅助函数,尝试解析 JSON 字符串并调用指定回调函数处理解析后的数据。如果解析失败,则记录错误。 使用示例:
import create from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})));
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
<div>
<span>{count}</span>
<button onClick={increment}>Increment</button>
</div>
);
}
在上述示例中,通过引入 devtools
中间件,可以在 Redux DevTools 中查看和调试状态变化。在每次 increment
操作后,可以在 DevTools 中查看最新的 count
值,并可以回滚、重置或查看状态变化轨迹。
immer
- 功能:支持使用 Immer 库实现不可变状态更新。
immer
中间件允许我们使用 Immer 库来管理不可变状态。immer
接受一个状态创建函数,并在内部通过produce
函数生成不可变的新状态。
const immerImpl = (initializer) => (set, get, api) => {
api.setState = (updater, replace, ...args) => {
const newState = typeof updater === 'function' ? produce(updater) : updater;
return set(newState, replace, ...args);
};
return initializer(api.setState, get, api);
};
export const immer = immerImpl;
使用示例:
const useStore = create(immer((set) => ({
count: 0,
increment: () => set(produce((state) => {
state.count += 1;
})),
})));
persist
- 功能:
persist
中间件用于实现状态持久化。它将 store 的状态保存在本地存储中(如localStorage
),并在应用重新加载时恢复状态。这对于确保页面刷新后状态保留特别有用。 persist
中间件的核心在于,它通过包装setState
方法,将状态变化同步到本地存储中。当应用重新加载时,它会从本地存储中恢复状态,并更新到 store 中。- 配置和存储接口:这定义了用于存储管理的接口。通过实现这些接口,可以自定义各种存储解决方案(如
localStorage
、sessionStorage
或其他异步存储)。
export interface StateStorage {
getItem: (name: string) => string | null | Promise<string | null>;
setItem: (name: string, value: string) => unknown | Promise<unknown>;
removeItem: (name: string) => unknown | Promise<unknown>;
}
export type StorageValue<S> = {
state: S;
version?: number;
}
- 创建 JSON 存储工具:这个工具函数通过传递 getStorage 函数来创建一个 JSON 存储工具。它负责序列化和反序列化状态。
export function createJSONStorage<S>(
getStorage: () => StateStorage,
options?: { reviver?: (key: string, value: unknown) => unknown, replacer?: (key: string, value: unknown) => unknown },
): PersistStorage<S> | undefined {
let storage: StateStorage | undefined;
try {
storage = getStorage();
} catch (e) {
return; // 在服务器端渲染时防止错误
}
const persistStorage: PersistStorage<S> = {
getItem: (name) => {
const parse = (str: string | null) => str ? JSON.parse(str, options?.reviver) as StorageValue<S> : null;
const str = storage?.getItem(name) ?? null;
return str instanceof Promise ? str.then(parse) : parse(str);
},
setItem: (name, newValue) => storage?.setItem(name, JSON.stringify(newValue, options?.replacer)),
removeItem: (name) => storage?.removeItem(name),
};
return persistStorage;
}
- 持久化中间件实现:
persistImpl
实现了状态的持久化功能。它通过api.setState
进行包装,当状态发生变化时会将其存储到持久化存储中。同时,在创建 store 时会尝试从持久化存储中恢复初始状态。
使用示例:通过 persist
中间件将 store 中的状态 count
持久化到 localStorage
中。每当 increment
方法更新状态时,状态也会被同步到本地存储中。
const useStore = create(persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'count-storage', // 定义存储名称
getStorage: () => localStorage, // 使用 localStorage 作为存储
}
));
redux
- 功能:
redux
中间件能够让 Zustand 模仿 Redux 风格的状态管理,提供类似的dispatch
和 reducer 支持,让开发者能够以 Redux 的方式去管理和更新状态。 redux
中间件的核心在于通过dispatch
方法将动作传递给 reducer,并根据 reducer 的返回值更新状态。这样可以将 Redux 的设计模式融入 Zustand 状态管理中。- 定义 Action 和 ReduxState 类型:下列类型定义了 Redux 风格的状态和动作。
Action
类型表示一个 Redux 动作对象,包含一个type
属性。StoreRedux
和ReduxState
类型定义了状态扩展的方法,允许在 Zustand 中使用dispatch
方法来分发动作。
type Action = { type: string };
type StoreRedux<A> = {
dispatch: (a: A) => A;
dispatchFromDevtools: true;
};
type ReduxState<A> = {
dispatch: StoreRedux<A>['dispatch'];
};
type WithRedux<S, A> = Write<S, StoreRedux<A>>;
- 中间件工厂函数:
const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => {
type S = typeof initial;
type A = Parameters<typeof reducer>[1];
// 定义 dispatch 方法
(api as any).dispatch = (action: A) => {
// 使用 set 方法调用 reducer,将新状态返回给 store
(set as NamedSet<S>)((state: S) => reducer(state, action), false, action);
return action;
};
(api as any).dispatchFromDevtools = true;
return { dispatch: (...args) => (api as any).dispatch(...args), ...initial };
};
export const redux = reduxImpl as unknown as Redux;
中间件 reduxImpl
分为以下几个步骤:
- 参数说明:接受
reducer
和initialState
作为参数。reducer
定义了如何根据动作更新状态,initialState
是初始状态。 - dispatch 方法:定义
dispatch
方法并增加到api
中,dispatch
方法接受一个动作,并调用传入的reducer
来计算新状态。通过调用set
方法将新状态存入 store 中,并通知所有监听器。 dispatchFromDevtools
属性:将dispatchFromDevtools
设置为 true,表示该 store 支持来自 Redux DevTools 的调试动作用。- 返回对象:返回新的状态对象,包含
dispatch
方法和初始状态。
使用示例:
import create from 'zustand';
import { redux } from 'zustand/middleware';
// 定义 reducer 函数
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
// 创建 store
const useStore = create(redux(reducer, { count: 0 }));
function Counter() {
const count = useStore((state) => state.count);
const dispatch = useStore((state) => state.dispatch);
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
- 定义
reducer
函数,处理INCREMENT
和DECREMENT
动作来更新状态中的count
。 - 使用
redux
中间件创建 Zustand store,通过传入reducer
和初始状态{ count: 0 }
。 - 在 React 组件中,使用
useStore
钩子获取当前状态和dispatch
方法,并在按钮点击时派发INCREMENT
和DECREMENT
动作来更新状态。
subscribeWithSelector
- 功能:
subscribeWithSelector
中间件允许通过选择器订阅状态变化,以便在特定的状态子集变化时触发回调。这种方式可以有效地提高性能,避免不必要的重新渲染。 subscribeWithSelector
中间件的核心在于扩展subscribe
方法,使其不仅能订阅整个状态的变化,还可以订阅通过选择器函数计算出的状态子集的变化。- 定义类型:该类型定义了扩展后的
subscribe
方法,提供两种订阅方式:一种是订阅整个状态的变化,另一种是通过选择器订阅状态子集的变化。
type StoreSubscribeWithSelector<T> = {
subscribe: {
(listener: (selectedState: T, previousSelectedState: T) => void): () => void;
<U>(
selector: (state: T) => U,
listener: (selectedState: U, previousSelectedState: U) => void,
options?: {
equalityFn?: (a: U, b: U) => boolean;
fireImmediately?: boolean;
},
): () => void;
};
};
- 核心逻辑:
const subscribeWithSelectorImpl: SubscribeWithSelectorImpl =
(fn) => (set, get, api) => {
type S = ReturnType<typeof fn>;
type Listener = (state: S, previousState: S) => void;
const origSubscribe = api.subscribe as (listener: Listener) => () => void;
api.subscribe = ((selector: any, optListener: any, options: any) => {
let listener: Listener = selector; // 如果没有选择器,直接使用传入的监听器
if (optListener) {
const equalityFn = options?.equalityFn || Object.is;
let currentSlice = selector(api.getState());
listener = (state) => {
const nextSlice = selector(state);
if (!equalityFn(currentSlice, nextSlice)) {
const previousSlice = currentSlice;
optListener((currentSlice = nextSlice), previousSlice);
}
};
if (options?.fireImmediately) {
optListener(currentSlice, currentSlice);
}
}
return origSubscribe(listener);
}) as any;
const initialState = fn(set, get, api);
return initialState;
};
- 扩展
subscribe
方法:扩展后的subscribe
方法接受选择器函数selector
和监听器optListener
。如果没有选择器函数,直接使用原始监听器。 - 选择器和比较函数:如果传入了选择器函数,则通过选择器计算出当前状态的 slice(子集)。定义一个新的监听器,当状态变化时,通过选择器计算下一个状态的 slice。如果新旧状态 slice 不相等(通过比较函数
equalityFn
比较),则调用传入的监听器。 - 立即触发监听器:如果传入了
fireImmediately
选项,则在订阅时立即调用监听器,传入当前 slice 和前一个 slice。 - 调用原始
subscribe
:最后调用原始subscribe
方法,传入包装后的监听器。
使用示例:
import create from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
const useStore = create(subscribeWithSelector((set) => ({
count: 0,
text: "",
increment: () => set((state) => ({ count: state.count + 1 })),
setText: (text) => set({ text }),
})));
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
<div>
<span>{count}</span>
<button onClick={increment}>Increment</button>
</div>
);
}
function TextInput() {
const text = useStore((state) => state.text);
const setText = useStore((state) => state.setText);
return (
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
);
}
- 定义一个 store,包括
count
、text
、increment
和setText
状态和方法。 - 使用
subscribeWithSelector
中间件创建 Zustand store。 - 在
Counter
组件中,订阅count
状态,并在按钮点击时调用increment
方法更新状态。 - 在
TextInput
组件中,订阅text
状态,并在输入框内容变化时调用setText
方法更新状态。
通过选择器订阅特定的状态子集,可以减少不必要的重新渲染,提高性能。
源码细节解析
zustand/vanilla 与 zustand/react 的差异
zustand/vanilla
zustand/vanilla
是 Zustand 的无依赖版本,它不依赖于 React,可以在任何 JavaScript 环境中运行。由于其无依赖性,zustand/vanilla
非常轻量且灵活,适用于 Node.js、React Native、服务端渲染 (SSR) 以及在 React 之外的纯 JavaScript 应用中使用。
zustand/vanilla
提供了核心的状态管理功能,包括创建 store、获取和设置状态、订阅状态变化等。它的 API 主要包括:
create
:用于创建一个新的 store。getState
:获取当前 store 的状态。setState
:部分或全部更新 store 的状态。subscribe
:订阅状态变化,当状态更新时调用订阅者的回调。destroy
:销毁 store 和取消所有订阅。
zustand/react
zustand/react
扩展了 zustand/vanilla
,并提供了与 React 的集成。在 zustand/react
中,新增了多个钩子和上下文,用于在 React 组件中更方便地访问和管理状态。 主要的扩展包括:
useStore
:这是一个自定义的 React hook,它是 zustand 与 React 集成最核心的部分,使得组件能够非常简便地订阅特定状态存储(store)的状态。通过使用这个 hook,组件只要在其内部调用相应的useStore
即可访问 store 的状态,而且会在这些状态更新时自动重新渲染,反映最新的状态。
通过这些扩展,开发者可以在 React 应用中使用 Zustand 管理状态,并享受 hooks 和上下文带来的便利。
状态获取与更新
zustand/vanilla
:通过getState
和setState
获取和更新状态。
const store = createStore((set, get) => ({
count: 0,
increment: () => set({ count: get().count + 1 })
}));
store.setState({ count: 1 });
console.log(store.getState().count); // 输出: 1
zustand/react
:通过useStore
hook 获取状态,并在 React 组件中使用。
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
状态订阅
zustand/vanilla
:使用subscribe
方法订阅状态变化,提供回调函数。
store.subscribe((state) => {
console.log(state.count);
});
zustand/react
:通过useStore
hook 订阅状态,使用选择器函数优化性能。
const count = useStore((state) => state.count);
状态清理
zustand/vanilla
:使用destroy
方法销毁 store 并取消所有订阅。
store.destroy();
zustand/react
:通常由 React 框架处理组件卸载时的清理工作。
集成 React DevTools
zustand/vanilla
:不支持直接的 DevTools 集成。zustand/react
:通过devtools
中间件,提供 Redux DevTools 支持。
内部使用的设计模式
观察者模式
Zustand 使用了观察者模式来管理状态。具体来说,store 的状态发生变化时,会通知所有订阅了该 store 的监听器。
// Subscribe to state changes
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
// Notify all subscribers when state changes
listeners.forEach((listener) => listener(state, previousState));
函数式编程
Zustand 强调函数式编程。通过传递纯函数(如 setState
、初始化函数)来创建和更新状态,同时保持状态不可变。
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState = typeof partial === 'function' ? (partial as (state: TState) => TState)(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = replace ? (nextState as TState) : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
中间件模式
通过中间件模式,zustand 允许在状态管理的不同阶段注入自定义逻辑。典型的中间件包括日志记录、持久化、DevTools 支持等。
const middleware = (config) => (set, get, api) => config((args) => {
console.log('prev state', get());
set(args);
console.log('new state', get());
}, get, api);
性能优化手段
部分订阅(Selector)
Zustand 支持选择器订阅,允许组件只监听状态的部分变化,从而减少不必要的重新渲染。
const count = useStore((state) => state.count);
通过选择器函数(如 (state) => state.count
),组件只会在 count
发生变化时重新渲染,而不会在其他状态变化时触发渲染。
浅比较优化
在 useStore
hook 中,通过浅比较(shallow compare)优化状态变化检测,避免无效渲染。
const useStore = (api, selector, equalityFn) => {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getInitialState,
selector,
equalityFn
);
return slice;
};
批量更新
可以利用 React 本身的批量更新功能,结合 Zustand 的 setState 方法来实现类似的效果。 React 18 以下版本已经引入了一个批处理更新的机制 (automatic batching
),这可以通过 ReactDOM 提供的 unstable_batchedUpdates
方法显式地触发。在 React 18 中,这个功能会自动进行。
import create from 'zustand';
// 创建我们初始的状态 store
const useStore = create((set) => ({
key1: "",
key2: "",
updateState: (newValue1, newValue2) =>
set((state) => ({ key1: newValue1, key2: newValue2 })),
}));
export default useStore;
在我们的 React 组件里,我们想要一次更新 key1 和 key2,可以这样去做:
import React from 'react';
import useStore from './path-to-store';
import { unstable_batchedUpdates } from 'react-dom';
function App() {
const { key1, key2, updateState } = useStore(state => ({
key1: state.key1,
key2: state.key2,
updateState: state.updateState,
}));
const handleClick = () => {
const newValue1 = "newKey1Value";
const newValue2 = "newKey2Value";
unstable_batchedUpdates(() => {
updateState(newValue1,newValue2);
});
};
return (
<div>
<p>{key1}</p>
<p>{key2}</p>
<button onClick={handleClick}>Update State</button>
</div>
);
}
export default App;
通过 unstable_batchedUpdates
进行 batched
处理,使在事件和 I/O 中的多个状态更新合并为一个更新,减少渲染次数。
错误处理机制
内部错误捕获
Zustand 通过 try-catch 块来捕获一些可能的异常。例如,在创建 store 时,如果 getStorage
方法出错,则会返回 undefined 并记录错误。这可以避免在服务器端渲染时由于无法访问 localStorage
而导致的错误。
let storage: StateStorage | undefined;
try {
storage = getStorage();
} catch (e) {
// prevent error if the storage is not defined (e.g. when server side rendering a page)
console.error(e);
}
警告通知
在一些可能引发问题的情况下,Zustand 会发出控制台警告来提醒开发者,如在生产模式下使用开发工具相关的中间件。
if (import.meta.env?.MODE !== 'production') {
console.warn('[zustand devtools middleware] Please install/enable Redux devtools extension');
}
通过这些机制,Zustand 确保无论是在客户端还是服务端环境下,都能更可靠地运行并提供有效的提示信息,帮助开发者快速定位和解决问题。
示例演示
简单的计数器示例
在这部分,我们将通过创建一个简单的计数器应用演示如何使用 Zustand 来管理状态。
创建 Zustand Store
创建一个文件 store.js 来定义我们的 Zustand store。
// store.js
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}));
export default useStore;
创建 React 组件
在组件中使用 Zustand 来管理和展示计数状态。我们创建一个名为 Counter.js
的文件。
// Counter.js
import React from 'react';
import useStore from './store';
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
使用 Counter 组件
在主应用入口文件中渲染我们的计数器组件。在 App.js
中:
// App.js
import React from 'react';
import Counter from './Counter';
function App() {
return (
<div>
<h1>Zustand Counter Example</h1>
<Counter />
</div>
);
}
export default App;
应用运行后,将显示计数器,并且可以通过点击按钮增加或减少计数。
数据流动与更新
状态更新
当调用 increment
或 decrement
方法时,store 状态发生变化,具体由 set 方法触发。
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
set
方法
set
方法负责部分或全部更新 store 的状态,并通知所有订阅者。 内部实现利用浅拷贝的方式进行状态合并保证状态的不变性。
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState = typeof partial === 'function' ? (partial as (state: TState) => TState)(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = replace ? (nextState as TState) : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
状态更新后
一旦状态更新,所有通过 subscribe
方法订阅了状态变化的监听器都会被调用,React 组件通过 useStore
再次获取最新的状态,并触发重新渲染以显示更新后的值。
组件重新渲染
React 组件通过 useStore
hook 订阅状态变化,并在 Zustand 内部通过 useSyncExternalStoreWithSelector
进行状态管理:
const useStore = (api, selector, equalityFn) => {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getInitialState,
selector,
equalityFn
);
return slice;
};
这种通过选择器和浅比较方式,确保只有相关状态变化时才会重新渲染组件,避免不必要的性能开销。
常见问题和答疑
常见使用问题及解决方案
1. 如何在多个组件间共享 Zustand 状态?
问题描述:如何在多个 React 组件之间共享 Zustand 的状态?
解决方案:使用 zustand
创建的 store 是全局的,因此可以在任何组件中通过 useStore
hook 访问和更新状态。
// store.js
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export default useStore;
// ComponentA.js
import React from 'react';
import useStore from './store';
function ComponentA() {
const count = useStore((state) => state.count);
return <div>Count: {count}</div>;
}
export default ComponentA;
// ComponentB.js
import React from 'react';
import useStore from './store';
function ComponentB() {
const increment = useStore((state) => state.increment);
return <button onClick={increment}>Increment</button>;
}
export default ComponentB;
2. 如何持久化 Zustand 状态?
问题描述:如何在应用刷新后保持 Zustand 的状态?
解决方案:可以使用 zustand/middleware
的 persist
中间件将状态存储到本地存储,如 localStorage
。
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'count-storage', // 存储的名字
getStorage: () => localStorage, // 存储位置
}
));
export default useStore;
3. 如何在 Zustand 中使用中间件?
问题描述:如何添加中间件,如 devtools、logger 等来扩展 Zustand?
解决方案:可以使用 zustand/middleware
提供的中间件如 devtools
、logger
等。
import create from 'zustand';
import { devtools, logger } from 'zustand/middleware';
const useStore = create(devtools(logger((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))));
export default useStore;
4. 如何在 Zustand 中使用异步操作?
问题描述:如何在 Zustand 中处理异步操作,如 API 请求?
解决方案:可以在状态更新函数内部处理异步操作,如 API 请求。
import create from 'zustand';
const useStore = create((set) => ({
items: [],
fetchItems: async () => {
const response = await fetch('https://api.example.com/items');
const data = await response.json();
set({ items: data });
},
}));
export default useStore;
5. 如何在数组或对象中更新特定的项?
问题描述:如何在数组或对象中仅更新特定的项?
解决方案:可以通过 setState
函数和 spread 操作符实现部分更新。
import create from 'zustand';
const useStore = create((set) => ({
users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
updateUser: (id, newName) =>
set((state) => ({
users: state.users.map(user => user.id === id ? { ...user, name: newName } : user)
})),
}));
export default useStore;
源码解析中可能遇到的困惑和解释
setState
函数内部是如何实现的?
1. 解释:setState
函数负责更新状态并通知所有订阅者。它的实现包括以下几个步骤:
- 计算新的状态(通过传入的部分状态或更新函数)。
- 如果新状态和旧状态不同,更新状态并通知所有订阅者。
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState = typeof partial === 'function' ? (partial as (state: TState) => TState)(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = replace ? (nextState as TState) : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
useStore
hook 如何与 React 组件交互?
2. 解释:useStore
hook 使用 useSyncExternalStore
或其 shim 来订阅 store 的状态变化,并在状态变化时触发组件重新渲染。
const useStore = (api, selector, equalityFn) => {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getInitialState,
selector,
equalityFn
);
return slice;
};
3. 中间件是如何实现和链式调用的?
解释:Zustand 中的中间件通过高阶函数实现,每个中间件接受一个 config
函数作为参数,并返回一个新的带有增强功能的 config
函数。多个中间件可以通过函数组合的方式进行链式调用。
const logger = (config) => (set, get, api) => config((args) => {
console.log('prev state', get());
set(args);
console.log('new state', get());
}, get, api);
const useStore = create(logger(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))));
subscribe
方法如何实现订阅机制?
4. 解释:subscribe
方法允许函数订阅 store 的状态变化,并在状态变化时调用订阅者函数。
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
5. 如何处理 Zustand 中的中间件顺序?
问题:在 Zustand 中使用多个中间件时,如何处理它们之间的顺序问题?
解释:中间件是通过高阶函数链式调用的顺序来实现的。因此,中间件的应用顺序是从里到外,例如:在这个例子中:
const useStore = create(logger(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))));
devtools
中间件最先应用,其功能会被logger
包裹。logger
中间件随后应用,包裹并增强devtools
。
不同的中间件提供的功能可以叠加,顺序影响日志记录、DevTools 调试等核心功能。
总结
Zustand 是一个轻量、灵活、高效的状态管理库,其设计优点主要体现在以下几个方面:
- 简洁易用:Zustand 的 API 非常简洁明了。通过
create
函数即可轻松创建并使用 store,减少了繁琐的样板代码。 - 无依赖性:
zustand/vanilla
部分不依赖任何其他库,可以独立运行在任何 JavaScript 环境中,允许在 Node.js、React Native 以及服务端渲染 (SSR) 中使用。 - React 集成:
zustand/react
提供了与 React 的深度集成,通过useStore
Hook 可以方便地在 React 组件中使用状态和订阅变更。 - 中间件支持:Zustand 轻松集成中间件,如持久化、DevTools 支持、日志记录等,极大扩展了库的功能。
- 高性能:通过选择器订阅和浅比较优化,确保了更细粒度的状态变更检测和最少的组件重新渲染,从而提升性能。
- 支持渐进增强:可在需要时逐步集成中间件和工具,如 Redux DevTools,仅保留需要的功能,不引入多余复杂度。
通过对 Zustand 源码的解析,有许多技巧、设计理念和编码习惯是值得我们学习和借鉴的。
- 编码技巧:
- 拓展性设计:Zustand 通过中间件机制为状态管理增加了极大的灵活性和可扩展性。这种设计模式使得功能扩展和代码组织更加清晰。
- 部分订阅和选择器:通过选择器函数订阅状态的特定片段,从而减少不必要的渲染,提高性能。
- 浅比较优化:使用浅比较检查状态的改变,以优化性能并避免不必要的渲染。
- 设计理念
- 单一职责原则:Zustand 遵守单一职责原则,将状态管理与组件逻辑分离。每个 store 专注于管理其自身的状态和逻辑,而组件负责呈现视图和处理用户交互。
- 函数式编程:Zustand 强调函数式编程,通过传递纯函数管理状态,不可变性确保状态更新的可预测性和稳定性。
- 最小化依赖:Zustand 设计为无依赖库,核心功能不依赖于第三方库,减少了外部依赖的复杂性和代码膨胀。
- 透明性和可调试性:通过集成 Redux DevTools 等中间件,Zustand 提供了强大的调试支持,使状态管理透明化,便于开发者追踪和调试状态变化。
- 编码习惯
- 代码简洁:Zustand 的源码非常简洁明了,避免了不必要的复杂性。通过简洁直观的 API 和实现,让开发者可以快速上手和理解。
- 详细注释:在复杂的实现部分,源码中加入了有用的注释和文档注释,帮助理解代码逻辑和设计意图。
- 错误处理:Zustand 在关键的代码部分添加了错误处理和警告信息,确保在异常情况下仍能正常工作,并提供有用的调试信息。
- 可测试性:Zustand 的架构设计使得其核心功能易于测试。例如:通过创建 vanilla store,模拟不同的状态和行为。使用 Jest 等测试库,可以方便地验证状态更新和订阅功能。