Zustand源码解读(更新中)

Zustand Store 对象底层实现

在深入研究 store 对象的具体实现之前,我们首先需要聚焦于其核心的 TypeScript 类型系统 。这套类型系统是理解 zustand 设计哲学和功能实现的关键。

一、类型系统:StoreApiSetStateInternal

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,它的两个重载分别对应了两种状态更新模式:

  1. 合并更新 ( replace?: false ) :这是默认的更新方式。
    • partial 参数可以是完整的状态对象 T
    • 也可以是部分状态对象 Partial<T>
    • 还可以是一个函数式更新 ,接受前一个状态 state 并返回新的状态。
  1. 替换更新 ( replace: true ) :当 replace 标志为 true 时,整个状态对象将被完全替换,因此 partial 参数必须是完整的状态对象 T

通过这种精巧的类型设计,zustandsetState 函数在提供灵活性的同时,也保证了严格的类型安全。

工具类型

在类型系统中,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)**机制。中间件能够拦截并增强 setgetapi 对象,从而为 store 附加新功能(如持久化、日志、immer 集成等)。这种增强在 TypeScript 层面需要一套复杂的类型操作来实现。

StoreMutators:中间件的类型扩展接口

中间件可能会给 storeapi 对象增加新的属性或方法。为了在类型层面反映这些变化,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 操作:

  1. 边界检查
    • 如果 Ms(中间件列表)是一个长度不确定的数组,直接返回原始 store 类型 S
    • 如果 Ms 是一个空数组 [],说明所有中间件都已处理完毕,返回当前的 store 类型 S
  1. 递归处理
    • 使用 infer 关键字从 Ms 数组中推断出第一个中间件的标识符 Mi附加类型 Ma,以及剩余的中间件列表 Mrs
    • 最关键的一步:StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier]。这里从 StoreMutators 中取出经过当前中间件 Mi 处理后的新 store 类型。
    • 将这个新的 store 类型剩余的中间件列表 Mrs 再次传入 Mutate,进行递归处理。
  1. 结束条件 :当最后一个中间件处理完毕后,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 的创建:createStorecreateStoreImpl

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 对象并初始化状态
};

核心逻辑解读

  1. 闭包状态statelisteners 被保存在闭包中,构成了 store 的私有状态。
  2. setState 实现
    • 它首先计算出 nextState
    • 使用 Object.is 进行浅比较,判断状态是否真的发生了变化。这是性能优化的关键,避免了不必要的更新。
    • replace ?? ... 逻辑决定了更新行为:
      • 如果 replace 被显式传递,则遵循其值。
      • 如果 replace 未传递(为 nullundefined),则会检查 nextState 是否为对象。如果不是对象(如 number, string 等),则强制进行替换;如果是对象,则通过 Object.assign 进行浅合并。
    • 最后,遍历 listeners 集合,调用所有订阅者函数,将新旧状态传递给它们。

zustand 内部的事件发布-订阅机制设计得非常简单。这并非疏忽,而是因为 zustand 巧妙地将状态更新的调度工作委托给了 React 框架本身(通过 useSyncExternalStore),我们将在下一节详细探讨。

四、React 集成层:createuseStore

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 是如何诞生的:

  1. 它首先通过 createStore 创建了一个纯粹的、与框架无关的 store 实例 (api)。
  2. 然后,它定义了 useBoundStore 这个 Hook,其内部调用了 useStore
  3. 最巧妙的是 Object.assign(useBoundStore, api)。这使得 useBoundStore 既是一个可以在组件中使用的 Hook,又是一个包含了 getState , setState 等方法的对象 。这就是为什么我们可以在组件外部通过 useUserStore.getState() 来访问状态。

useStoreuseSyncExternalStore:安全订阅外部状态

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 接受三个核心参数:

  1. subscribe (api.subscribe):订阅函数 。React 会将一个内部的 forceUpdate 函数传递给它。当外部状态变化时(即 zustandsetState 被调用并通知订阅者时),这个 forceUpdate 会被触发,从而调度一个新的 React 渲染。
  2. getSnapshot (() => selector(api.getState())):获取快照函数 。这个函数的作用是从外部数据源获取当前状态的快照 。React 会在渲染开始时调用它,并用其返回值作为组件的当前状态。它还会用 Object.is 比较前后两次快照的结果,如果相同则跳过更新,以优化性能。
  3. getServerSnapshot (() => selector(api.getInitialState())):获取服务端快照函数 。这个参数主要用于解决**服务器端渲染(SSR)和客户端水合(Hydration)**时不一致的问题。

例子 :一个用户的登录状态保存在 localStorage 中。在服务端渲染时,由于无法访问 localStorage,服务器只能生成一个"未登录"状态的 HTML。然而,当客户端接收到 HTML 并进行水合时,它会从 localStorage 读取到"已登录"状态,导致服务端和客户端的初始 UI 不匹配,出现闪烁。

getServerSnapshot 允许我们在服务端提供一个权威的状态快照(例如,通过解析请求头中的 Cookie 来判断登录状态),确保服务端生成的 HTML 与客户端的初始状态一致。

useSyncExternalStore 的工作流程可以概括为

  1. 初始渲染 :调用 getSnapshot 获取初始状态,并使用 subscribe 订阅外部 store 的变化。
  2. 外部状态更新zustandsetState 触发 subscribe 注册的回调(即 React 的 forceUpdate)。
  3. React 调度更新:React 将组件标记为需要重渲染。
  4. 重渲染 :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 的状态持久化到存储介质(如 localStorageAsyncStorage)中。它提供了丰富的配置项来满足各种复杂场景。

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: 指定存储介质,默认为 localStoragezustand 提供了 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.stringifyJSON.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 逻辑,实现了一个功能强大且配置灵活的数据持久化方案。

相关推荐
天蓝色的鱼鱼16 小时前
前端开发者的组件设计之痛:为什么我的组件总是难以维护?
前端·react.js
XiaoSong19 小时前
从未有过如此丝滑的React Native开发体验:EAS开发构建完全指南
前端·react.js
用户76787977373221 小时前
后端转全栈之Next.js数据获取与缓存
react.js·next.js
小仙女喂得猪1 天前
2025 Android原生开发者角度的React/ReactNative 笔记整理
react native·react.js
艾小码1 天前
为什么你的页面会闪烁?useLayoutEffect和useEffect的区别藏在这里!
前端·javascript·react.js
骑自行车的码农1 天前
【React用到的一些算法】游标和栈
算法·react.js
小高0071 天前
🔍说说对React的理解?有哪些特性?
前端·javascript·react.js
江城开朗的豌豆1 天前
从生命周期到useEffect:我的React函数组件进化之旅
前端·javascript·react.js
江城开朗的豌豆1 天前
React组件传值:轻松掌握React组件通信秘籍
前端·javascript·react.js