Zustand源码解读(一)

简单解读 zustand 源码(一)

zustand 作为一个非常轻量级的 react 状态管理库受到很多人的喜欢,而且 zustand 的源码非常精简(除了 ts 部分,详细原因维护者有一些讲解 (opens in a new tab)

所以今天的简单解读一下 zustand 的源码,学习一下设计思路 看源码之前需要明确几件事情:

  1. 源码方面由于 zustand 的 ts 体操非常丑陋,所以主要看打包后的 js,而不看 ts 实现,ts 只会作为临时的类型参考
  2. 用法方面主要看 js 用法,而不看 ts 用法,ts 由于类型推导有一些问题,如果想看为什么 ts 的用法和 js 的有区别,参考官方 (opens in a new tab)

从基本用法说起

官方给出的 js 基本用法如下

import { create } from "zustand";
 
const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));
function BearCounter() {
  const bears = useBearStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
}
 
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}
 
//我自己常用的方法
function BearCounter() {
  const { bears } = useBearStore();
  return <h1>{bears} around here ...</h1>;
}

从上面可以到主要用法就是在 ts 文件里 create 一个具有操作方法的 store, 而 create 接受一个函数,函数第一个参数为 set 在使用上主要是直接引用 store,挑选出自己需要使用的方法或者值

源码主要流程解析

我下载了 4.3.7 的 npm 包并且创建了一个仓库 (opens in a new tab),有需要可以在这里看到我的完整笔记,笔记为英文,因为笔者未来打算同步写英文博客 首先打开 esm/index.js 文件,可以看到 create 被 export 出来,从这里开始读起

  • create 方法
// start here , consider createState is passed to the func , another situation is for ts
const create = (createState) =>
  createState ? createImpl(createState) : createImpl;

上面的代码显示是否需要传入 createState,实际上这里是给 ts 作为类型推导的(详情看明确部分),js 使用中 createState 一定会传入,那么下面是 createImpl 的代码

  • createImpl 方法
// this is what create(createState) returned
const createImpl = (createState) => {
  // some warning , pass
  if (
    process.env.NODE_ENV !== "production" &&
    typeof createState !== "function"
  ) {
    console.warn(
      "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`."
    );
  }
  // if you pass a func , then pass the func to createStore, otherwise return createState
  // consider func is passed , because we always want to use set & get func in store func
  const api =
    typeof createState === "function" ? createStore(createState) : createState;
  const useBoundStore = (selector, equalityFn) =>
    // key to not use provider , this takes advantages of useSyncExternalStore of react
    // check out for more
    // https://react.dev/reference/react/useSyncExternalStore
    useStore(api, selector, equalityFn);
  // use assign the api method to useBoundStore
  Object.assign(useBoundStore, api);
  return useBoundStore;
};

首先代码先判断了在 proc 环境以及 createState,并给出了一个 warning,createState 需要为一个函数,非函数参数会被未来废弃掉 然后来到了最重要的部分,下面定义了一个 api,当我们传入 createState 为 func 类型时,createStore(createState)被执行,并被赋值给 api。 createStore 在 esm/vanilla.js 中

  • createStore 方法
// this is the api in index.js , createState is a func that contains set & get
const createStore = (createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl;

还是有一层判断,所以这里判定 createStoreImpl(createState)

  • createStoreImpl
const createStoreImpl = (createState) => {
  let state;
  const listeners = /* @__PURE__ */ new Set();
  // declare all useful funcs that createState would use : setState, getState , api
  // setState: partial means the part of storeObj , replace is a boolean that determine if you replace the whole store
  const setState = (partial, replace) => {
    // sometimes we pass a mutate func rather than a target obj
    const nextState = typeof partial === "function" ? partial(state) : partial;
    // whether nextState and state is same in memory address , often not same
    // if not same
    if (!Object.is(nextState, state)) {
      const previousState = state;
      // if replace is not provided & nextState is not an obj , then clone state and nextState to a new obj
      // if replace is not provided & nextState is an obj , then return nextState
      // change state to new one
      state = (replace != null ? replace : typeof nextState !== "object")
        ? nextState
        : Object.assign({}, state, nextState);
      // after the operation , exec listeners according to order
      listeners.forEach((listener) => listener(state, previousState));
    }
  };
  // just return current state
  const getState = () => state;
  // listeners could be added through subscribe
  // returned func could be used to unsubscribe
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  //clear all listeners
  const destroy = () => {
    if (process.env.NODE_ENV !== "production") {
      console.warn(
        "[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."
      );
    }
    listeners.clear();
  };
  // return api obj
  const api = { setState, getState, subscribe, destroy };
  // initialize state
  state = createState(setState, getState, api);
  return api;
};

首先看 createStoreImpl 的变量定义,一共定义了变量:state,listeners,setState,getState,subscribe,destroy

  1. state 就是最终的 state,里面包含了用户定义的所有变量
  2. listeners 是用户自定义的监听器
  3. setState 是更新变量方法
  4. getState 是获取变量方法
  5. subscribe 和 destroy 分别用来操作 listeners

其中最重要的是 setState

const setState = (partial, replace) => {
  // sometimes we pass a mutate func rather than a target obj
  const nextState = typeof partial === "function" ? partial(state) : partial;
  // whether nextState and state is same in memory address , often not same
  // if not same
  if (!Object.is(nextState, state)) {
    const previousState = state;
    // if replace is not provided & nextState is not an obj , then clone state and nextState to a new obj
    // if replace is not provided & nextState is an obj , then return nextState
    // change state to new one
    state = (replace != null ? replace : typeof nextState !== "object")
      ? nextState
      : Object.assign({}, state, nextState);
    // after the operation , exec listeners according to order
    listeners.forEach((listener) => listener(state, previousState));
  }
};

首先 setstate 接受两个参数,一个是需要更新的变量,另一个是是否需要 replace 对象的 boolean

  1. 函数首先判断是否 partial 是一个函数,如果是,那么执行这个函数,如果不是,直接返回 partial,总之最后的 nextstate 是一个对象
  2. 判断是否 nextstate 和 state 是同一个对象,如果是,那么直接结束函数,state 不变。但一般我们不会传入当前 state,那么现在开始更新 state
  3. 首先当没有传入 replace 时,并且 nextstate 不是对象时,创建一个新对象并把 state 和 nextstate 合并进去,如果 nextstate 是对象,那么直接吧 nextstate 赋值给 state。
  4. 如果传入了 replace,操作相同,判断条件不同
  5. 经过上面的赋值步骤之后,开始执行所有的 listerners,函数结束

createStoreImpl 最后定义了 api,里面上面定义的四个方法:setState, getState, subscribe, destroy 并且定义 state 为 createState(setState, getState, api),这里是初始化 state 的步骤 然后返回 api

让我们回到 createImpl 方法,现在我们知道 api 其实是具有四个方法的一个对象 下一步 createImpl 方法定义了 useBoundStore 方法,并返回了 useStore 的返回值,useStore 实际上是调用了 react 的内置useSyncExternalStoreWithSelector (opens in a new tab)方法,这个方法是专门用来给第三方库使用的,因为有了这个方法所以 zustand 不需要用 context provider,详情请参阅 react。

函数最后把 api Object.assign 给了 useBoundStore 至此我们的 store 创建完成并且得到了 useBoundStore useBoundStore 的类型为

export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
  (): ExtractState<S>;
  <U>(
    selector: (state: ExtractState<S>) => U,
    equals?: (a: U, b: U) => boolean
  ): U;
} & S;

也就是说我们可以像官方推荐的一样,传入一个 selector,也可以像我常用的一样,直接执行这个函数然后返回整个对象 下面提供一个官方使用的例子和流程对应

const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))
 
// api that in createImpl
// Getting non-reactive fresh state
const paw = useDogStore.getState().paw
// Listening to all changes, fires synchronously on every change
const unsub1 = useDogStore.subscribe(console.log)
// Updating state, will trigger listeners
useDogStore.setState({ paw: false })
// Unsubscribe listeners
unsub1()
 
// You can of course use the hook as you always would
const Component = () => {
  const paw = useDogStore((state) => state.paw)
  ...

总结

本文总结了 zustand 的简单使用,并且根据简单使用讲解了源码中的流程。

后续会讲解 middleware 的源码讲解