Zustand Store 对象底层实现
在深入研究 store 对象的具体实现之前,我们首先需要聚焦于其核心的 TypeScript 类型系统 。这套类型系统是理解 zustand 设计哲学和功能实现的关键。
一、类型系统:StoreApi 与 SetStateInternal
StoreApi<T>:Store 的核心接口
store 对象(在源码中常被称为 api)的类型是通过 StoreApi<T> 接口定义的。这个接口是整个 zustand 状态管理的核心,它规定了 store 对象必须具备的四个基本方法。
typescript
export interface StoreApi<T> {
setState: SetStateInternal<T>;
getState: () => T;
getInitialState: () => T;
subscribe: (listener: (state: T, prevState: T) => void) => () => void;
}
- 泛型参数
T:代表了我们应用中状态对象的具体类型,也就是creator函数返回的那个对象的类型。大多数情况下,TypeScript 的类型推断系统能自动识别这个类型。但当状态结构复杂时,建议手动指定以确保类型安全。 getState()和getInitialState():这两个函数相对直观,分别用于获取当前状态和初始状态,返回值都是状态类型T。subscribe():这是一个典型的发布-订阅模式实现。它接受一个订阅者(listener) 函数作为参数,并返回一个用于 取消订阅的函数。setState:这是最核心的状态更新函数,其类型定义SetStateInternal<T>相对复杂,我们接下来详细解析。
SetStateInternal<T>:函数重载的巧妙设计
setState 的类型定义利用了 TypeScript 的高级特性,以支持多种调用方式,同时保证类型安全。
css
type SetStateInternal<T> = {
_(partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'], replace?: false): void;
_(state: T | { _(state: T): T }['_'], replace: true): void;
}['_'];
初看之下,这个语法可能令人困惑。它实际上是函数重载 的一种高级定义方式。通过定义一个对象类型,并立即取出其 _ 属性的类型,我们得到了一个合并后的函数签名。
为什么不直接使用函数重载?
你可能会问,为什么不直接写成这样:
这种写法在大多数情况下是可行的,但当函数参数的类型依赖于函数自身的返回值类型时,就会遇到循环引用的问题。例如:
在 UpdateFn 的定义尚未完成时,ReturnType<UpdateFn> 是未知的,TypeScript 无法解析,因此会报错。为了解决这个问题,我们可以利用对象属性的延迟解析特性:
TypeScript 允许在对象属性的定义中引用同一对象的其他属性,只要不构成死循环即可。这为我们解决循环引用问题提供了优雅的方案。
typescript
// 传统的函数重载定义
type Add = {
(a: number, b: number): number; // 重载1:两个数字相加
(a: string, b: string): string; // 重载2:两个字符串拼接
};
typescript
type UpdateFn = (
updater: State | ((prev: State) => State),
callback?: (newState: ReturnType<UpdateFn>) => void // ❌ 错误:循环引用
) => State;
typescript
type UpdateFnWrapper = {
_: (
updater: State | ((prev: State) => State),
// 回调参数类型引用了对象属性 `_` 的返回值,而非直接引用 UpdateFn
callback?: (newState: ReturnType<UpdateFnWrapper['_']>) => void
) => State;
};
回到 SetStateInternal,它的两个重载分别对应了两种状态更新模式:
- 合并更新 (
replace?: false) :这是默认的更新方式。
-
partial参数可以是完整的状态对象T。- 也可以是部分状态对象
Partial<T>。 - 还可以是一个函数式更新 ,接受前一个状态
state并返回新的状态。
- 替换更新 (
replace: true) :当replace标志为true时,整个状态对象将被完全替换,因此partial参数必须是完整的状态对象T。
通过这种精巧的类型设计,zustand 的 setState 函数在提供灵活性的同时,也保证了严格的类型安全。
工具类型
在类型系统中,zustand 还定义了一些辅助工具类型,以便在后续的类型推导和操作中使用。
typescript
// 用于从 store 或 api 中推断出 state 的类型
export type ExtractState<S> = S extends { getState: () => infer T } ? T : never;
// 安全地获取对象属性类型,如果不存在则返回备用类型 F
type Get<T, K, F> = K extends keyof T ? T[K] : F;
二、中间件与类型扩展
zustand 的一大核心特性是其强大的**中间件(Middleware)**机制。中间件能够拦截并增强 set、get 和 api 对象,从而为 store 附加新功能(如持久化、日志、immer 集成等)。这种增强在 TypeScript 层面需要一套复杂的类型操作来实现。
StoreMutators:中间件的类型扩展接口
中间件可能会给 store 的 api 对象增加新的属性或方法。为了在类型层面反映这些变化,zustand 引入了 StoreMutators 接口。有趣的是,它在源码中最初是一个空接口:
csharp
export interface StoreMutators<S, A> {}
这是一个巧妙的设计,利用了 TypeScript interface 的**声明合并(Declaration Merging)**特性。开发者(或中间件库)可以向这个接口中不断添加新的类型定义,从而"注册"自己的中间件类型。
swift
// 开发者可以像这样扩展中间件类型
export interface StoreMutators<S, A> {
devtools: Write<Cast<S, object>, { devtools: A }>;
persist: Write<S, { persist: A }>;
immer: Write<Cast<S, object>, object>;
subscribeWithSelector: Write<S, { subscribe: A }>;
// 可以扩展更多中间件
}
这个接口就像一个类型层面的 switch...case 语句。当你需要为一个 store 类型 S 应用一个中间件时,你提供中间件的标识符(如 persist)和它所附加的类型 A,然后从 StoreMutators 中取出对应的转换后类型。
这里用到了两个关键的工具类型:
Cast<T, U>:T extends U ? T : U,确保类型T兼容类型U。Write<T, U>:Omit<T, keyof U> & U,用U中的属性覆盖T中的同名属性,实现类型的"写入"或"更新"。
Mutate<S, Ms>:递归处理中间件类型列表
我们通常会使用多个中间件,zustand 需要一个方法来按顺序将中间件类型依次应用到 store 类型上。这就是 Mutate<S, Ms> 的作用。
typescript
export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
? S
: Ms extends []
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs]
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never;
这个类型的工作逻辑类似于 JavaScript 中的数组 reduce 操作:
- 边界检查:
-
- 如果
Ms(中间件列表)是一个长度不确定的数组,直接返回原始store类型S。 - 如果
Ms是一个空数组[],说明所有中间件都已处理完毕,返回当前的store类型S。
- 如果
- 递归处理:
-
- 使用
infer关键字从Ms数组中推断出第一个中间件的标识符Mi和附加类型Ma,以及剩余的中间件列表Mrs。 - 最关键的一步:
StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier]。这里从StoreMutators中取出经过当前中间件Mi处理后的新store类型。 - 将这个新的
store类型 和剩余的中间件列表Mrs再次传入Mutate,进行递归处理。
- 使用
- 结束条件 :当最后一个中间件处理完毕后,
Mrs会变成空数组,递归进入第二个分支,返回最终被所有中间件增强过的store类型。
StateCreator<T, Mis, Mos>:状态创建函数的类型
StateCreator 是定义我们状态和业务逻辑的核心函数。它的类型定义也与中间件紧密相关。
typescript
export type StateCreator<
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T,
> = ((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) => U) & { $$storeMutators?: Mos };
zustand 将中间件分为两类:
Mis(Inbound Middlewares) :对creator函数的参数 (set,get,store)进行修改的中间件。Mos(Outbound Middlewares) :对creator函数的返回值(即最终的状态对象)进行修改的中间件。
注意最后一行 & { $$storeMutators?: Mos }。这行代码巧妙地将 Mos(出站中间件)的信息附加到了函数对象自身的一个特殊属性 $$storeMutators 上。
当 zustand 创建最终状态时,它会检查这个 $$storeMutators 属性。如果发现有记录,就会调用相应的逻辑来处理 creator 函数返回的原始状态,并返回处理后的结果。整个过程是类型驱动 的:TypeScript 确保修改器的类型正确,而 zustand 的运行时逻辑则确保这些修改被正确执行。
三、Store 的创建:createStore 与 createStoreImpl
store 对象的创建是通过 createStore 函数完成的。这个函数在内部实际调用了 createStoreImpl。让我们先从它们的类型定义开始。
createStore:灵活的柯里化设计
createStore 函数的设计非常灵活,它既可以接受一个 creator 函数直接创建 store,也可以不传递参数,返回一个"待创建"的函数(实际上就是 createStoreImpl)。这种设计通过函数重载实现。
ini
type CreateStore = {
// 重载 1: 接受 creator 函数,直接创建 store
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): Mutate<StoreApi<T>, Mos>;
// 重载 2: 不接受参数,返回一个可以创建 store 的函数(柯里化)
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) => Mutate<StoreApi<T>, Mos>;
};
函数的实际实现巧妙地利用了 JavaScript 的特性来匹配这个类型定义:
javascript
export const createStore = ((createState) =>
createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore;
关于调用签名中的泛型 <T>
你可能会注意到,在类型定义中存在泛型 <T>,但在实际实现中却没有。这是 TypeScript 的一个高级特性。这里的 <T> 是函数调用签名 的一部分,而不是类型别名 CreateStore 本身的一部分。
CreateStore 类型描述了一个"契约":任何被标记为此类型的函数,在被调用时,其泛型 <T> 的具体类型将由 TypeScript 根据传入的 initializer 参数自动推断出来。
ini
type CreateArray = {
// 这里的 <T> 是函数"调用签名"的一部分
<T>(input: T): T[];
};
// 我们将一个函数断言为 CreateArray 类型
const createArrayFn: CreateArray = (input) => {
return [input, input];
};
// 当我们"调用"这个函数时,T 才被推断出来
const numberArray = createArrayFn(100); // T 被推断为 number
const stringArray = createArrayFn("world"); // T 被推断为 string
createStoreImpl:核心实现
createStoreImpl 是实际创建 store 的函数,它必须接收一个 creator 函数作为参数。
ini
type CreateStoreImpl = <
T,
Mos extends [StoreMutatorIdentifier, unknown][] = [],
>(
initializer: StateCreator<T, [], Mos>,
) => Mutate<StoreApi<T>, Mos>;
其实现逻辑非常简洁,是 zustand 底层发布-订阅模式的核心:
typescript
const createStoreImpl: CreateStoreImpl = (createState) => {
// 推断出状态类型 TState
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;
// 根据 replace 标志决定是替换还是合并状态
state =
(replace ?? (typeof nextState !== 'object' || nextState === null))
? (nextState as TState)
: Object.assign({}, state, nextState);
// 通知所有订阅者
listeners.forEach((listener) => listener(state, previousState));
}
};
// ... 创建 api 对象并初始化状态
};
核心逻辑解读:
- 闭包状态 :
state和listeners被保存在闭包中,构成了store的私有状态。 setState实现:
-
- 它首先计算出
nextState。 - 使用
Object.is进行浅比较,判断状态是否真的发生了变化。这是性能优化的关键,避免了不必要的更新。 replace ?? ...逻辑决定了更新行为:
- 它首先计算出
-
-
- 如果
replace被显式传递,则遵循其值。 - 如果
replace未传递(为null或undefined),则会检查nextState是否为对象。如果不是对象(如number,string等),则强制进行替换;如果是对象,则通过Object.assign进行浅合并。
- 如果
-
-
- 最后,遍历
listeners集合,调用所有订阅者函数,将新旧状态传递给它们。
- 最后,遍历
zustand 内部的事件发布-订阅机制设计得非常简单。这并非疏忽,而是因为 zustand 巧妙地将状态更新的调度工作委托给了 React 框架本身(通过 useSyncExternalStore),我们将在下一节详细探讨。
四、React 集成层:create 与 useStore
zustand/react 模块提供了将核心 store 与 React 组件连接起来的桥梁。其入口是 create 函数,它是一个比 createStore 更高层次的封装。
create 函数:连接 Store 与 React
create 函数的定义与 createStore 类似,同样运用了柯里化和泛型推导。
javascript
// react.ts
export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create;
它在内部调用 createImpl,这个函数是集成逻辑的核心:
typescript
// react.ts
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
// 1. 使用我们之前分析的 createStore 创建一个底层的 store api
const api = createStore(createState);
// 2. 创建一个在组件中使用的 Hook
const useBoundStore: any = (selector?: any) => useStore(api, selector);
// 3. 将底层 api 的方法(getState, setState, subscribe等)附加到 Hook 上
Object.assign(useBoundStore, api);
return useBoundStore;
};
这段代码揭示了我们日常使用的 useUserStore 这样的 Hook 是如何诞生的:
- 它首先通过
createStore创建了一个纯粹的、与框架无关的store实例 (api)。 - 然后,它定义了
useBoundStore这个 Hook,其内部调用了useStore。 - 最巧妙的是
Object.assign(useBoundStore, api)。这使得useBoundStore既是一个可以在组件中使用的 Hook,又是一个包含了getState,setState等方法的对象 。这就是为什么我们可以在组件外部通过useUserStore.getState()来访问状态。
useStore 与 useSyncExternalStore:安全订阅外部状态
useStore 是连接 zustand 和 React 的关键,它的核心是 React 18 引入的 useSyncExternalStore Hook。
scss
// react.ts
export function useStore<TState, StateSlice>(
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
) {
const slice = React.useSyncExternalStore(
api.subscribe,
React.useCallback(() => selector(api.getState()), [api, selector]),
React.useCallback(() => selector(api.getInitialState()), [api, selector]),
);
React.useDebugValue(slice);
return slice;
}
为什么需要 useSyncExternalStore ?
React 的 Fiber 架构允许渲染过程被中断和重启。当组件的状态由 React 自身管理时(如 useState),React 能保证数据的一致性。但当状态来自外部系统 (如 zustand store、localStorage、甚至一个普通的全局变量)时,就可能出现**状态撕裂(State Tearing)**的问题。
例子 :一个组件在渲染时从外部 store 读取了值为 5。此时,渲染被中断。在渲染重启之前,外部 store 的值变成了 6。当组件恢复渲染时,它可能会获取到混乱的数据,导致 UI 与状态不一致。
useSyncExternalStore 就是为了解决这个问题而生的。它提供了一个安全的 API,让 React 能够订阅外部数据源,并确保在并发渲染模式下数据的一致性。
useSyncExternalStore 接受三个核心参数:
subscribe(api.subscribe):订阅函数 。React 会将一个内部的forceUpdate函数传递给它。当外部状态变化时(即zustand的setState被调用并通知订阅者时),这个forceUpdate会被触发,从而调度一个新的 React 渲染。getSnapshot(() => selector(api.getState())):获取快照函数 。这个函数的作用是从外部数据源获取当前状态的快照 。React 会在渲染开始时调用它,并用其返回值作为组件的当前状态。它还会用Object.is比较前后两次快照的结果,如果相同则跳过更新,以优化性能。getServerSnapshot(() => selector(api.getInitialState())):获取服务端快照函数 。这个参数主要用于解决**服务器端渲染(SSR)和客户端水合(Hydration)**时不一致的问题。
例子 :一个用户的登录状态保存在 localStorage 中。在服务端渲染时,由于无法访问 localStorage,服务器只能生成一个"未登录"状态的 HTML。然而,当客户端接收到 HTML 并进行水合时,它会从 localStorage 读取到"已登录"状态,导致服务端和客户端的初始 UI 不匹配,出现闪烁。
getServerSnapshot 允许我们在服务端提供一个权威的状态快照(例如,通过解析请求头中的 Cookie 来判断登录状态),确保服务端生成的 HTML 与客户端的初始状态一致。
useSyncExternalStore 的工作流程可以概括为:
- 初始渲染 :调用
getSnapshot获取初始状态,并使用subscribe订阅外部store的变化。 - 外部状态更新 :
zustand的setState触发subscribe注册的回调(即 React 的forceUpdate)。 - React 调度更新:React 将组件标记为需要重渲染。
- 重渲染 :React 再次调用
getSnapshot获取最新的状态,并与上一次的快照比较。如果不同,则更新组件,UI 展示最新状态。
通过这个机制,zustand 将状态管理的逻辑与 React 的渲染调度解耦,实现了高效、安全的状态同步。
React 相关的类型定义
在 react.ts 中,类型定义也相应地变得更加具体。
scala
// 限制只能访问 store 的只读方法,防止在组件中意外调用 setState
type ReadonlyStoreApi<T> = Pick<
StoreApi<T>,
'getState' | 'getInitialState' | 'subscribe'
>;
// 我们日常使用的 store hook 的完整类型
export type UseBoundStore<S extends ReadonlyStoreApi<unknown>> = {
// 调用时不带 selector,返回整个 state
(): ExtractState<S>;
// 调用时带 selector,返回 state 的一部分
<U>(selector: (state: ExtractState<S>) => U): U;
} & S; // & S 使得 hook 本身也挂载了 getState 等方法
// create 函数的完整类型签名
type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>;
// ... 柯里化重载
};
五、中间件实现:以 persist 为例
中间件的核心思想是对 set 、 get 和 api 对象进行增强 。我们以 zustand 最常用的 persist 中间件为例,深入剖C析其实现细节。
persist 中间件:功能与配置
persist 中间件用于将 store 的状态持久化到存储介质(如 localStorage 或 AsyncStorage)中。它提供了丰富的配置项来满足各种复杂场景。
javascript
const useUserStore = create(
persist(
// 1. 原始的状态创建函数
(set, get) => ({
// ... state definition
}),
// 2. 持久化配置对象
{
name: 'user-storage-v2', // 存储的 key (必填)
storage: createJSONStorage(() => customAsyncStorage), // 存储介质
partialize: (state) => ({ ... }), // 筛选需要持久化的字段
version: 2, // 当前状态版本
migrate: (persistedState, version) => { ... }, // 版本迁移函数
merge: (persistedState, currentState) => { ... }, // 自定义状态合并规则
onRehydrateStorage: (state) => { ... }, // 水合回调
// skipHydration: false, // 跳过自动水合
}
)
);
核心配置项解读:
name: 在存储介质中使用的唯一键名。storage: 指定存储介质,默认为localStorage。zustand提供了createJSONStorage工具函数来包装存储引擎。partialize: 一个函数,用于从完整状态中筛选出需要持久化的部分。这对于排除临时状态(如isEditing)非常有用。version&migrate: 用于状态迁移。当你的应用更新,数据结构发生变化时,migrate函数可以帮助你将旧版本的数据平滑地过渡到新结构。merge: 定义从存储中恢复的状态(persistedState)如何与当前代码中的初始状态(currentState)合并。默认是浅合并。
工具函数
在深入 persist 的核心逻辑之前,我们需要了解两个关键的工具函数。
toThenable:统一同步与异步操作
存储介质可能是同步的(如 localStorage)或异步的(如 IndexedDB)。为了用一套统一的逻辑来处理这两种情况,persist 使用了 toThenable 函数,它能将任何值或函数调用结果包装成一个类似 Promise 的对象(Thenable)。
javascript
const toThenable = (fn) => (input) => {
try {
const result = fn(input);
// 如果结果本身就是 Promise,直接返回
if (result instanceof Promise) {
return result;
}
// 否则,包装成一个 Thenable 对象
return {
then(onFulfilled) {
// 在 .then 中继续链式调用
return toThenable(onFulfilled)(result);
},
catch(_onRejected) {
return this;
},
};
} catch (e) {
// 捕获同步错误,并将其转换到 catch 链
return {
then(_onFulfilled) {
return this;
},
catch(onRejected) {
return toThenable(onRejected)(e);
},
};
}
};
这个函数通过柯里化和闭包,巧妙地模拟了 Promise 的链式调用和错误处理机制,使得后续的 hydrate 流程可以像处理 Promise 一样,通过 .then() 和 .catch() 来编排同步和异步操作。
createJSONStorage:简化存储操作
这个函数包装了标准的 Storage API,自动处理了 JSON.stringify 和 JSON.parse 的过程,并兼容同步和异步的 getItem 方法。
javascript
export function createJSONStorage(getStorage, options = {}) {
let storage;
try {
storage = getStorage();
} catch {
// 在 SSR 等环境下,存储可能不可用
return;
}
return {
getItem: (name) => {
const parse = (str) => {
if (str === null) return null;
return JSON.parse(str, options.reviver);
};
const str = storage.getItem(name) ?? null;
// 如果 storage.getItem 返回的是 Promise,则链式调用 parse
if (str instanceof Promise) {
return str.then(parse);
}
// 否则直接解析
return parse(str);
},
setItem: (name, newValue) => {
return storage.setItem(name, JSON.stringify(newValue, options.replacer));
},
removeItem: (name) => storage.removeItem(name),
};
}
persistImpl:核心实现逻辑
persist 中间件的实现可以分为两个主要部分:状态持久化( setItem ) 和 状态水合( hydrate ) 。
dart
const persistImpl = (config, baseOptions) => (set, get, api) => {
// ... 初始化 options
// 1. 定义核心的 setItem 函数
const setItem = () => {
// 使用 partialize 筛选要存储的状态
const stateToPersist = options.partialize({ ...get() });
// 使用 storage 引擎存储
return storage.setItem(options.name, {
state: stateToPersist,
version: options.version,
});
};
// 2. 重写 setState 和 creator 函数的 set 方法
// 使得每次状态更新后都会自动调用 setItem
const savedSetState = api.setState;
api.setState = (state, replace) => {
savedSetState(state, replace);
return setItem();
};
const configResult = config(
(...args) => {
set(...args);
return setItem();
},
get,
api
);
// ... hydrate 逻辑
return configResult;
};
状态水合 (hydrate)
水合(Hydration)是 persist 中间件最复杂也最重要的部分。它负责在应用启动时,从存储介质中读取数据,并将其恢复到 store 中。整个过程被精心编排在一个 toThenable 链中。
javascript
const hydrate = () => {
if (!storage) return;
// ... 触发 onHydrate 回调
return toThenable(storage.getItem.bind(storage))(options.name)
// 步骤 1: 读取、迁移数据
.then((deserializedStorageValue) => {
if (deserializedStorageValue) {
// 版本不匹配,执行 migrate 函数
if (
typeof deserializedStorageValue.version === 'number' &&
deserializedStorageValue.version !== options.version
) {
if (options.migrate) {
// ... 执行 migrate 逻辑,可能返回 Promise
return [true, migratedState]; // [migrated, state]
}
} else {
// 版本一致,直接使用存储的状态
return [false, deserializedStorageValue.state];
}
}
// 无存储数据
return [false, undefined];
})
// 步骤 2: 合并状态并更新 store
.then(([migrated, migratedState]) => {
// 使用 merge 函数合并存储状态与当前状态
stateFromStorage = options.merge(migratedState, get() ?? configResult);
// 强制替换 store 的状态
set(stateFromStorage, true);
// 如果执行了迁移,则将迁移后的新状态写回存储
if (migrated) {
return setItem();
}
})
// 步骤 3: 完成处理
.then(() => {
// ... 触发 onFinishHydration 回调
})
.catch((e) => {
// ... 错误处理
});
};
暴露控制 API
最后,persist 中间件还通过 api.persist 向外暴露了一系列控制方法,允许我们在应用中手动控制持久化行为。
javascript
api.persist = {
// 动态更新配置
setOptions: (newOptions) => { ... },
// 清空存储
clearStorage: () => { ... },
// 手动触发水合
rehydrate: () => hydrate(),
// 检查是否已水合
hasHydrated: () => hasHydrated,
// 监听水合开始/结束
onHydrate: (cb) => { ... },
onFinishHydration: (cb) => { ... },
};
通过以上分析,我们可以看到 persist 中间件如何通过重写 setState、利用 toThenable 统一异步流程、以及精巧的 hydrate 逻辑,实现了一个功能强大且配置灵活的数据持久化方案。