通过 Zustand 源码学习 TS:Zustand 实现原理很简单,TS 类型写的是真的强

2024-11-04 08:30   重庆  

点击关注公众号,“技术干货” 及时达!

前言

zustand 是一个非常轻量的 react 状态管理库,借力了 react hooks,源码非常精简,使用方法也很简单。

store 创建

zustand 通过调用 create 方法创建 store,以 create 方法为入口来看下 store 的创建逻辑。

export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>  createState ? createImpl(createState) : createImpl) as Create;

在上面的代码中 createState 是一个可选参数,它的类型为 StateCreator<T, [], []> | undefined

  • StateCreator<T, [], []>:这是一个函数类型,负责创建状态的初始值。StateCreator 通常是一个函数,用于定义状态的形状和行为。它可以包括初始状态以及状态操作的定义。

  • undefined:因为 createState 是一个可选参数,所以它也可以是 undefined。

如果 createState 存在(即传入了状态创建函数),则调用 createImpl(createState)。createImpl 是一个内部实现的函数,用于根据 createState 创建状态。如果 createState 不存在(即未传入状态创建函数),则直接返回 createImpl 函数本身。这种情况通常用于对状态管理逻辑进行进一步的配置。

这段代码的作用是根据传入的 createState(如果有的话)来创建状态对象,并返回一个状态管理函数。通过使用泛型和类型断言,这个 create 函数能够灵活地支持各种类型的状态,并且在没有传入 createState 时,提供了一种配置状态管理的方式。

不传入 createState 的主要用途是在需要更高的灵活性时,通过获取底层的 createImpl 函数来实现自定义的状态初始化和扩展。这种方式能够在应用中支持多种复杂的状态管理需求,如延迟初始化、动态创建、多实例化以及中间件集成等。

StateCreator 类型

StateCreator 这个类型设计到的比较多,我们在学习 Zustand 的同时顺便把 ts 也学习了:

type SetStateInternal<T> = {  _(    partial: T | Partial<T> | { _(state: T): T | Partial<T> }["_"],    replace?: false  ): void;  _(state: T | { _(state: T): T }["_"], replace: true): void;}["_"];
export interface StoreApi<T> { setState: SetStateInternal<T>; getState: () => T; getInitialState: () => T; subscribe: (listener: (state: T, prevState: T) => void) => () => void;}
type Get<T, K, F> = K extends keyof T ? T[K] : F;
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;
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 };
export interface StoreMutators<S, A> {}export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>;

SetStateInternal<T> 类型

type SetStateInternal<T> = {  _(    partial: T | Partial<T> | { _(state: T): T | Partial<T> }["_"],    replace?: false  ): void;  _(state: T | { _(state: T): T }["_"], replace: true): void;}["_"];

SetStateInternal<T> 定义了一种内部更新状态的方法。['_'] 是一种获取重载类型的技巧,用来使 setState 支持两种更新方式:

  1. _(partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'], replace?: false): void:参数 partial 可以是完整的状态对象 T,部分状态 Partial<T>,或者是一个函数,返回新的状态或部分状态。replace 是一个可选的布尔值,如果为 false 或不传入,则表示部分更新。

  2. _(state: T | { _(state: T): T }['_'], replace: true): void:当 replace 为 true 时,表示完全替换状态。参数 state 是新的完整状态。

在 TypeScript 中,函数参数具有逆变的特性。这意味着 _ 方法的参数类型越宽泛,越可以被其他函数类型替换。对于 SetStateInternal类型的 _ 方法:

  • 第一个参数 partial 或 state 被设计为接受多种类型(T、Partial<T>、或者函数形式的状态更新)。这里体现了逆变的特性,因为接受一个更宽泛的类型集合。

  • 例如,如果 T 是 { count: number },那么 Partial<T>{ count?: number },而 { (state: T): T | Partial<T> }["_"] 是更复杂的类型。这些参数类型从具体类型到较宽泛的类型,符合逆变的定义。

对于返回类型 void,函数的重载都返回 void,没有任何变形的体现。在这个例子中,返回类型没有实际的协变或逆变意义。

在 TypeScript 的函数参数中,默认情况下参数是双变的。这意味着参数既可以是它的子类型,也可以是它的超类型。在 SetStateInternal的实现中,函数的参数默认支持双变性,这使得类型系统更灵活。

例如,对于 _ 方法的第一个重载:partial 可以是类型 T,也可以是 Partial<T>,这实际上就是利用了 TypeScript 的双变性,允许我们传入更宽泛或更具体的类型。

假设我们有一个简单的状态结构:

interface State {  count: number;  name: string;}
const setState: SetStateInternal<State> = (partial, replace) => { if (replace) { // 完全替换状态 console.log("Replacing state with:", partial); } else { // 部分更新状态 console.log("Updating state with:", partial); }};
// 使用部分更新setState({ count: 10 }, false); // 输出: Updating state with: { count: 10 }
// 使用完全替换setState({ count: 20, name: "Alice" }, true); // 输出: Replacing state with: { count: 20, name: 'Alice' }
// 使用状态更新函数setState((state) => ({ count: state.count + 1 }), false); // 输出: Updating state with: function

在这个例子中,SetStateInternal<T> 可以根据参数来决定如何更新状态,支持多种形式的状态更新。

Get<T, K, F> 类型

type Get<T, K, F> = K extends keyof T ? T[K] : F;

Get 是一个条件类型,用于从类型 T 中提取键 K 的类型。如果 K 存在于 T 中,则返回 T[K];否则,返回默认类型 F。常用于根据键名动态获取类型,且可以在键不存在时提供一个默认类型。

如下示例所示:

interface User {  id: number;  name: string;}
type UserNameType = Get<User, "name", string>; // 返回类型为 stringtype UserAgeType = Get<User, "age", number>; // 返回类型为 number(因为 'age' 不存在)

Mutate<S, Ms> 类型

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;

Mutate<S, Ms> 是一个递归类型,用于对状态类型 S 应用一系列的“变换器”(mutators)

Ms 是一个数组类型,其中每个元素表示一个 [Mi, Ma] 元组:

  1. Mi 是变换器的标识符。

  2. Ma 是变换器的参数类型。

我们再来对这个 ts 类型做一个详细的讲解:

  1. number extends Ms['length' & keyof Ms]:检查 Ms 是否为一个动态长度的数组。如果是,则返回原始状态类型 S。

  2. Ms extends []:如果 Ms 是空数组,则表示没有变换,返回原始状态 S。

  3. Ms extends [[infer Mi, infer Ma], ...infer Mrs]:提取第一个 mutator 元组 [Mi, Ma],剩余的存入 Mrs。使用 StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier] 对状态类型进行变换。递归调用 Mutate,继续对剩余的 mutators 进行处理。

我们再来看一下下面的 ts 类型定义:

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;
// 2. 定义 StoreMutatorIdentifier 类型,用于获取 StoreMutators 键名export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>;
interface StoreMutators<S, A> { logging: (state: S, action: A) => S; timestamp: (state: S, action: A) => S;}
type LoggingMutator = ["logging", void];type TimestampMutator = ["timestamp", void];
// 应用多个 Mutatortype TransformedState = Mutate< { count: number }, [LoggingMutator, TimestampMutator]>;

在 Zustand 中,这段代码用于类型级别的状态变换,用来管理和应用各种 "Mutator"。Mutator 是一种状态变换器,可以通过特定的方式修改状态。通过这段代码,Zustand 能够:

  1. 增强状态功能:可以为状态增加一些额外的功能,比如日志记录、时间戳、撤销/重做等功能。这些增强功能可以通过不同的 Mutator 来实现。

  2. 支持插件式扩展:不同的 Mutator 可以看作是对状态管理的插件,通过 Mutate 类型,Zustand 可以在类型级别上灵活地应用多个 Mutator。这种设计允许开发者根据需求选择和组合不同的功能,从而增强状态管理的能力。

  3. 实现类型安全的状态变换:通过 Mutate 类型的递归应用,Zustand 能够确保状态的类型在经过多个变换器后保持正确性,避免在使用状态时出现类型不匹配的问题。

我们再来看一下下面这段代码的使用示例:

// 1. 定义 Mutate 类型,用于递归地应用变换器数组 Ms 到状态类型 Sexport 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;
// 2. 定义 StoreMutatorIdentifier 类型,用于获取 StoreMutators 键名export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>;
// 3. 定义 StoreMutators 接口,其中包含两个变换器 logging 和 timestampinterface StoreMutators<S, A> { logging: (state: S, action: A) => S; timestamp: (state: S, action: A) => S;}
// 4. 定义 LoggingMutator 和 TimestampMutator 类型,分别表示一个 mutator 及其参数类型type LoggingMutator = ["logging", void];type TimestampMutator = ["timestamp", void];
// 5. 实际的 mutators 实现,包含 logging 和 timestamp 的逻辑const mutators: StoreMutators<any, any> = { logging: (state, action) => { console.log("Logging state:", state); return state; // 返回未修改的状态 }, timestamp: (state, action) => { return { ...state, timestamp: new Date().toISOString(), // 添加时间戳 }; },};
// 6. 定义 applyMutators 函数,用于依次应用给定的变换器列表function applyMutators<S, Ms extends [StoreMutatorIdentifier, unknown][]>( initialState: S, mutatorsList: Ms): Mutate<S, Ms> { let state = initialState;
for (const [mutatorId, action] of mutatorsList) { const mutator = mutators[mutatorId as StoreMutatorIdentifier]; if (mutator) { state = mutator(state, action); } }
return state as Mutate<S, Ms>;}
// 7. 使用 applyMutators 函数对状态进行变换const initialState = { count: 0 };
// 定义变换器列表,并使用 LoggingMutator 和 TimestampMutator 类型进行类型约束const transformedState = applyMutators< typeof initialState, [LoggingMutator, TimestampMutator]>(initialState, [ ["logging", undefined], ["timestamp", undefined],]);
console.log("Transformed state:", transformedState);

在上面的这种代码展示了一种状态管理系统的设计模式,特别适用于在应用中需要动态地修改、增强或扩展状态的场景。通过使用"变换器"(mutators),可以对状态进行按需的修改,同时保持类型安全。

目前这段代码实现的功能,更接近于插件系统的设计。通过定义变换器(mutators),我们可以动态地对状态进行增强或修改,这种机制类似于为状态管理添加插件。通过传入不同的变换器列表(mutators),可以在状态管理过程中动态地添加或移除功能。例如,我们可以根据需求添加日志记录、时间戳等功能,也可以根据不同的应用场景使用不同的变换器组合。

我们再来实现一个中间件的案例,我们需要了解一下中间件的特点,在经典的中间件模式中:

  1. 中间件可以拦截状态的变化,并在处理前后执行一些逻辑。

  2. 每个中间件都可以选择是否继续传递控制权给下一个中间件。

  3. 中间件的执行顺序通常是按定义的顺序依次执行。

为了让当前代码更符合中间件的特点,可以引入以下改进:

  1. 增加中间件控制权:允许中间件决定是否继续执行下一个中间件。

  2. 添加 next 函数:为每个中间件提供一个 next 函数,使其可以显式地调用下一个中间件。

下面是对现有代码的修改,以实现一个更符合中间件模式的版本:

// 1. 定义 Mutate 类型,用于递归地应用变换器数组 Ms 到状态类型 Sexport 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;
// 2. 定义 StoreMutatorIdentifier 类型,用于获取 StoreMutators 键名export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>;
// 3. 定义 StoreMutators 接口,其中包含两个变换器 logging 和 timestampinterface StoreMutators<S, A> { logging: (state: S, action: A, next: (newState: S) => S) => S; timestamp: (state: S, action: A, next: (newState: S) => S) => S;}
// 4. 定义 LoggingMutator 和 TimestampMutator 类型,分别表示一个 mutator 及其参数类型type LoggingMutator = ["logging", void];type TimestampMutator = ["timestamp", void];
// 5. 实际的 mutators 实现,包含 logging 和 timestamp 的逻辑const mutators: StoreMutators<any, any> = { logging: (state, action, next) => { console.log("Logging state:", state); return next(state); // 调用 next 并传入当前状态 }, timestamp: (state, action, next) => { const newState = { ...state, timestamp: new Date().toISOString(), // 添加时间戳 }; return next(newState); // 使用更新后的状态调用 next },};
// 6. 定义 applyMiddlewares 函数,用于按顺序执行中间件列表function applyMiddlewares<S, Ms extends [StoreMutatorIdentifier, unknown][]>( initialState: S, middlewaresList: Ms): Mutate<S, Ms> { let index = 0; // 当前中间件的索引
// 定义一个递归函数,依次调用每个中间件 const execute = (state: S): S => { if (index >= middlewaresList.length) { return state; // 所有中间件执行完毕,返回最终的状态 }
const [mutatorId, action] = middlewaresList[index++]; const mutator = mutators[mutatorId as StoreMutatorIdentifier]; if (mutator) { // 调用当前的中间件,并传入 next 函数 return mutator(state, action, (newState) => execute(newState)); } return execute(state); // 如果没有找到 mutator,则继续执行下一个 };
return execute(initialState) as Mutate<S, Ms>;}
// 7. 使用 applyMiddlewares 函数对状态进行变换const initialState = { count: 0 };
// 定义中间件列表,并使用 LoggingMutator 和 TimestampMutator 类型进行类型约束const transformedState = applyMiddlewares< typeof initialState, [LoggingMutator, TimestampMutator]>(initialState, [ ["logging", undefined], ["timestamp", undefined],]);
console.log("Transformed state:", transformedState);

最终输出结果如下所示:

在上面的代码的执行流程中:

  1. logging 中间件打印初始状态 { count: 0 },并将状态传递给下一个中间件。

  2. timestamp 中间件在状态中添加 timestamp 字段,然后将新的状态传递给下一个中间件。

  3. 最终,变换后的状态是 { count: 0, timestamp: "2024-10-10T10:00:00.000Z" }。

通过这些修改,现在的中间件系统可以按顺序对状态进行变换,每个中间件都可以对状态进行修改,并将修改后的状态传递给下一个中间件。这使得系统更符合中间件的设计模式,具有更强的灵活性和扩展性。

StateCreator

StateCreator 定义了一个函数签名,用于创建状态的函数。

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 };

我们先来对泛型参数来做一个简单的解释:

  1. T: 初始状态的类型。

  2. Mis: 输入的 mutators 列表(默认值为空数组)。这些 mutators 用于在创建状态时进行状态的变换或增强。

  3. Mos: 输出的 mutators 列表(默认值为空数组)。用于表示经过变换后的状态增强列表。

  4. U: 返回的状态类型,默认与 T 相同。这意味着状态创建函数可以返回一个不同类型的状态。

StateCreator 定义了一个函数签名,表示一个状态创建函数,接收以下三个参数:

  1. setState: 用于更新状态的方法,其类型是从 Mutate<StoreApi<T>, Mis> 中获取的 setState。

  2. getState: 用于获取当前状态的方法,类型同样来源于 Mutate<StoreApi<T>, Mis>

  3. store: 经过所有输入的 mutators 变换后的 StoreApi 类型。

函数返回值为 U 类型的状态。这表示状态创建函数在初始化状态时可以执行自定义的逻辑,并返回一个不同的状态类型。

$$storeMutators 属性是可选的,用于存储变换器的元信息,即变换后的 mutators 列表。这为 zustand 提供了一种跟踪和管理状态增强的机制。

StateCreator 类型允许开发者在创建状态时传入多个 mutators,这些 mutators 会按顺序应用到状态管理的实例上,增强状态的功能。通过 Mutate 和 Get 等类型工具,StateCreator 可以动态地根据输入的 mutators 列表调整 setState 和 getState 方法的类型签名。

我们可以通过 Mis 和 Mos 来定义一个状态管理函数,该函数会使用输入的 mutators 进行状态的变换,并追踪输出的变换器列表。

下面是一个示例,演示如何使用带有 Mis 和 Mos 的状态创建函数,在创建状态时动态地增强状态管理实例。

import { create } from "zustand";
// 1. 定义变换器接口和类型
// 变换器接口,描述状态管理过程中可以使用的变换器interface StoreMutators<S, A> { logging: (state: S, action: A) => S; timestamp: (state: S, action: A) => S;}
// 变换器类型,用于描述各个变换器的具体参数type LoggingMutator = ["logging", { level: "info" | "warn" | "error" }];type TimestampMutator = ["timestamp", { format: "iso" | "unix" }];
// 2. 定义状态类型interface State { count: number; timestamp?: string; increment: () => void; // 添加 increment 方法的定义}
// 3. 定义输入和输出的 mutators 列表type Mis = [LoggingMutator, TimestampMutator]; // 输入的变换器列表type Mos = [LoggingMutator, TimestampMutator]; // 输出的变换器列表
// 4. 实现具体的变换器逻辑const mutators: StoreMutators<any, any> = { logging: (state, action) => { const { level } = action; switch (level) { case "info": console.info(`Logging state change at level: ${level}`, state); break; case "warn": console.warn(`Logging state change at level: ${level}`, state); break; case "error": console.error(`Logging state change at level: ${level}`, state); break; default: console.log(`Logging state change at level: ${level}`, state); } return state; // 不改变状态,仅记录日志 }, timestamp: (state, action) => { const { format } = action; const newTimestamp = format === "iso" ? new Date().toISOString() : Date.now().toString(); return { ...state, timestamp: newTimestamp, // 更新状态,添加时间戳 }; },};
// 5. 创建状态管理函数,并使用带有 Mis 和 Mos 的状态创建函数export const useStore = create<State>((set, get) => ({ count: 0, increment: () => { set((state) => { // 应用 mutators let newState = state; newState = mutators.logging(newState, { level: "info" }); newState = mutators.timestamp(newState, { format: "iso" });
// 返回变更后的状态 return { ...newState, count: state.count + 1 }; }); },}));
"use client";
import { useStore } from "@/store";import React from "react";
function App() { const { count, increment, timestamp } = useStore((state) => ({ count: state.count, increment: state.increment, timestamp: state.timestamp, }));
console.log(count, timestamp);
return ( <div> <p>Count: {count}</p> <p>Timestamp: {timestamp}</p> <button onClick={increment}>添加</button> </div> );}
export default App;

现在,点击 添加 按钮时,计数器值会增加,并在控制台输出相应的日志信息,同时界面上会更新显示的 count 和 timestamp 值。

通过这个示例,我们展示了如何使用 zustand 的 Mis 和 Mos 来应用和追踪变换器对状态管理的增强。这样可以在创建状态时灵活地对状态管理系统进行扩展,使得状态管理更具灵活性和可扩展性

createImpl

create 函数讲解完成之后,我们顺着往下来到 createImpl 代码:

const createImpl = <T>(createState: StateCreator<T, [], []>) => {  const api = createStore(createState);
const useBoundStore: any = (selector?: any) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;};

在 createImpl 里面最后返回了 useBoundStore 方法,这个方法基于 useStore 和 api:

  1. useStore: 基于 react hook 中的 useSyncExternalStore,react 状态管理库其核心之一就是状态改变时如何触发更新渲染,像 react-redux、或者原生 createContext 亦或者 forceUpdate,都是间接调用 setState 方法去触发更新,而 useSyncExternalStore 是官方提供的另一种状态更新方案,这个在文章的前文有相关的链接。
export function useStore<S extends ReadonlyStoreApi<unknown>, U>(  api: S,  selector: (state: ExtractState<S>) => U): U;
export function useStore<TState, StateSlice>( api: ReadonlyStoreApi<TState>, selector: (state: TState) => StateSlice = identity as any) { const slice = React.useSyncExternalStore( api.subscribe, () => selector(api.getState()), () => selector(api.getInitialState()) ); React.useDebugValue(slice); return slice;}
  1. api 对象:调用 createStore 方法并传入 createState 参数得到 api 对象(这儿假定传入的 createState 是一个函数)。

调用 createStore ,实则是将 createState 参数透传给 createStoreImpl 方法(这儿同样忽略 createState 不存在的情况,源码里面之所以对 createState 不存在的情况下也做了处理,是为了让开发者自定义 store 而已)。

在 createStore 中调用的 createStoreImpl 方法同样定义在 src/vanilla.ts 文件中:

type CreateStoreImpl = <  T,  Mos extends [StoreMutatorIdentifier, unknown][] = []>(  initializer: StateCreator<T, [], Mos>) => Mutate<StoreApi<T>, Mos>;
const createStoreImpl: CreateStoreImpl = (createState) => { type TState = ReturnType<typeof createState>; type Listener = (state: TState, prevState: TState) => void; let state: TState; // listener函数集合,用Set结构保证同一个listener函数只添加一次,避免重复添加 const listeners: Set<Listener> = new Set();
// 更新state状态并调用注册到集合里面的所有listener,触发更新渲染, // 至于调用listener如何触发的更新就涉及到react hook的useSyncExternalStore了 const setState: StoreApi<TState>["setState"] = (partial, replace) => { // 如果partial是函数,则调用函数得到nextState,不然直接赋值partial给nextState const nextState = typeof partial === "function" ? (partial as (state: TState) => TState)(state) : partial; // 通过比对nextState和state决定是否更新state状态以及更新触发渲染 if (!Object.is(nextState, state)) { const previousState = state; // 如果replace为真值,直接替换原来的state,否则合并nextState到state state = replace ?? (typeof nextState !== "object" || nextState === null) ? (nextState as TState) : Object.assign({}, state, nextState);
// 调用listener触发更新渲染 listeners.forEach((listener) => listener(state, previousState)); } };
// 提供获取state的方法 const getState: StoreApi<TState>["getState"] = () => state;
// 获取初始状态。initialState 会在函数最后被赋值为状态的初始值。 const getInitialState: StoreApi<TState>["getInitialState"] = () => initialState;
// 添加listener,返回值提供删除listener的方法 // 其实这个subscribe会在react useEffect里面执行往Set集合里面添加listener,这个return对应的就是useEffect里面的return, const subscribe: StoreApi<TState>["subscribe"] = (listener) => { listeners.add(listener); // Unsubscribe return () => listeners.delete(listener); };
const api = { setState, getState, getInitialState, subscribe }; const initialState = (state = createState(setState, getState, api)); return api as any;};

createStoreImpl 代码可以看出:通过 zustand 管理的状态 state 实则就是 createState 在通过 create(createState) ⇒ createImpl(createState) ⇒ createStore(createState) ⇒ createStoreImpl(createState) 透传后在 createStoreImpl 里面被执行后返回的那个返回值,而在 createStoreImpl 函数里面最后返回的并不是这个状态 state ,而是一个合并了更新 setState、访问 getState、订阅函数 subscribe、销毁函数 destroy 的 api 对象,管理的状态 state 实则就是以闭包的形式存在。

再回头看调用 create 创建的 store,这个 store 就是一个函数方法,对应 createImpl(createState) 里面的 useBoundStore(const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn),更准确的说是一个自定义的 react hook,这个在后面会阐述,和 redux 的 useSelector 使用方法是一样的),只是在这个函数方法上叠加了 api 对象的属性。

每一次调用 useXXX 都会去执行 create 函数,创建那么多份不会内存泄漏吗?

这个问题是引自另外一篇文章的评论区问题 看完 zustand 源码后,我的 TypeScript 水平突飞猛进。

既然看过源码,我能问个问题吗? 当你创建 store 是这样创建的const useXXX = cteate(set=>{...})这就意味着每一次调用 useXXX 都会去执行 create 函数,创建那么多份不会内存泄漏吗?还是说底层保证了每次调用都是那一份?能看看怎么保证的吗?

在 zustand 中,create 函数用于创建一个状态管理的 store,并返回一个 useStore 钩子函数。当你使用 const useXXX = create(set => {...})来创建 store 时,create 函数只会被执行一次,并且 zustand 的内部机制会确保每次调用 useXXX 时都是使用同一个 store 实例,而不会重复创建多个 store。因此,不会出现内存泄漏问题。

当你调用 create 函数时,zustand 会创建一个状态管理的实例,并返回一个用于访问状态的 useStore 钩子函数。create 函数的具体流程如下:

  1. create 函数内部会调用一个函数(如 createStoreImpl),来实际创建并初始化一个状态管理的 store 实例。这个实例包含了所有管理状态的方法(如 setState、getState、subscribe 等),并会在内存中保持对这些方法的引用。

  2. zustand 会通过一个闭包将这个 store 实例包装在 useStore 钩子函数中,并返回这个钩子函数。每次调用 useStore 时,都是访问同一个已经创建的 store 实例,而不会重新创建。

  3. 在调用 useStore 时,zustand 会使用 React 的 useSyncExternalStore 来确保组件与状态之间的订阅关系。通过 subscribe 方法,zustand 会将每个订阅的组件与状态的变化关联起来,当状态变化时,只有需要更新的组件会重新渲染。

create 函数只会执行一次,返回的 useStore 钩子函数会保持对 store 实例的引用。无论你在组件中使用多少次 useStore,访问的都是同一个 store 实例。并且它是通过 subscribe 方法,zustand 可以在组件卸载时自动移除监听器,防止订阅关系残留。当组件卸载时,zustand 会自动取消该组件的订阅,不会导致内存泄漏。

export function useStore<TState, StateSlice>(  api: ReadonlyStoreApi<TState>,  selector: (state: TState) => StateSlice = identity as any) {  const slice = React.useSyncExternalStore(    api.subscribe,    () => selector(api.getState()),    () => selector(api.getInitialState())  );  React.useDebugValue(slice);  return slice;}

因此 create 函数只会执行一次,并返回一个 useStore 钩子函数,该函数始终引用同一个 store 实例。这个 store 实例包含了所有的状态管理方法(如 setState、getState、subscribe 等),并且在内存中保存。

每次调用 useStore 时,通过内部的 useSyncExternalStore 钩子,管理与 store 的订阅。组件会订阅 store 的状态变化,当状态发生改变时,useSyncExternalStore 会触发重新渲染。无论组件中使用 useStore 调用多少次,它们访问的都是最初通过 create 创建的那个 store 实例。

参考资料

  • 两张图带你全面了解 React 状态管理库:zustand 和 jotai

  • Zustand 源码浅析

  • 看完zustand源码后,我的TypeScript水平突飞猛进。

总结

zustand 通过一个简单的状态存储对象 (store) 和函数式的 API 提供了灵活的状态管理方式。它的核心特点是轻量、灵活、易于集成,并通过 useSyncExternalStore 实现高效的状态订阅和组件重渲染机制。此外,zustand 还支持中间件扩展功能,如日志记录、持久化和调试工具集成,使其适应现代 React 的并发模式和服务器端渲染(SSR)的需求。

最后贴上 Zustand 的实现流程图:

通过学习 Zustand 源码,大概率能够提升你的 ts 技能,赶紧学起来吧。

最后分享两个我的两个开源项目,它们分别是:

  • 前端脚手架 create-neat
  • 在线代码协同编辑器

这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗

点击关注公众号,“技术干货” 及时达!

稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
 最新文章