简单解读 zustand 源码(一)
zustand 作为一个非常轻量级的 react 状态管理库受到很多人的喜欢,而且 zustand 的源码非常精简(除了 ts 部分,详细原因维护者有一些讲解 (opens in a new tab))
所以今天的简单解读一下 zustand 的源码,学习一下设计思路 看源码之前需要明确几件事情:
- 源码方面由于 zustand 的 ts 体操非常丑陋,所以主要看打包后的 js,而不看 ts 实现,ts 只会作为临时的类型参考
- 用法方面主要看 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
- state 就是最终的 state,里面包含了用户定义的所有变量
- listeners 是用户自定义的监听器
- setState 是更新变量方法
- getState 是获取变量方法
- 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
- 函数首先判断是否 partial 是一个函数,如果是,那么执行这个函数,如果不是,直接返回 partial,总之最后的 nextstate 是一个对象
- 判断是否 nextstate 和 state 是同一个对象,如果是,那么直接结束函数,state 不变。但一般我们不会传入当前 state,那么现在开始更新 state
- 首先当没有传入 replace 时,并且 nextstate 不是对象时,创建一个新对象并把 state 和 nextstate 合并进去,如果 nextstate 是对象,那么直接吧 nextstate 赋值给 state。
- 如果传入了 replace,操作相同,判断条件不同
- 经过上面的赋值步骤之后,开始执行所有的 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 的源码讲解