如何使用

先来简单看看如何使用

 1import { create } from 'zustand'
 2
 3const useStore = create((set) => ({
 4  count: 1,
 5  inc: () => set((state) => ({ count: state.count + 1 })),
 6}))
 7
 8function Counter() {
 9  const { count, inc } = useStore()
10  return (
11    <div>
12    <span>{count}</span>
13    <button onClick={inc}>one up</button>
14    </div>
15  )
16}

这是官网给我们的一个例子

我们可以看到他是用一个create来创建一个store的

之前我已经搭建好了调试环境zustand调试 直接开看🤫,很易读大家可以跟着文章一起来调试着看

为了方便大家看我帮大家剔除了源码中的ts,源码中的ts还是蛮复杂的🙃

创建store

可以看到create接受一个createState就是我们之前的传入的函数

 1export const create = ((createState) =>
 2  createState ? createImpl(createState) : createImpl)

因为我们传入的createState是个函数所以会走到里createImpl(createState)继续看下去 参道参道 我们先不关注后面的代码,我们可以看到由于我们传的createState是个函数它又调用了createStore这个函数

 1const createImpl = (createState) => {
 2  const api =
 3    typeof createState === 'function' ? createStore(createState) : createState
 4
 5  const useBoundStore: any = (selector?: any, equalityFn?: any) =>
 6    useStore(api, selector, equalityFn)
 7
 8  Object.assign(useBoundStore, api)
 9
10  return useBoundStore
11}

createStore如下,很熟悉是不是,感觉和俄罗斯套娃一样🙃

 1export const createStore = ((createState) =>
 2  createState ? createStoreImpl(createState) : createStoreImpl)

继续往下走就看到了核心逻辑createStoreImpl,好长啊,别急慢慢看,我们分段来看

 1const createStoreImpl: CreateStoreImpl = (createState) => {
 2  let state;
 3  const listeners = new Set();
 4
 5
 6  const setState = (partial, replace) => {
 7  
 8    const nextState =
 9      typeof partial === 'function'
10        ? partial(state)
11        : partial
12       
13    if (!Object.is(nextState, state)) {
14      const previousState = state
15      state =
16      
17        replace ?? (typeof nextState !== 'object' || nextState === null)
18          ? (nextState as TState)
19          
20          : Object.assign({}, state, nextState)
21       
22      listeners.forEach((listener) => listener(state, previousState))
23    }
24  }
25
26  const getState = () => state
27
28  const getInitialState= () =>
29    initialState
30
31  const subscribe = (listener) => {
32    listeners.add(listener)
33    
34    return () => listeners.delete(listener)
35  }
36
37  const destroy = () => {
38    listeners.clear()
39  }
40
41  const api = { setState, getState, getInitialState, subscribe, destroy }
42  const initialState = (state = createState(setState, getState, api))
43  return api
44}

我们可以看到他先创建了一个state,一个listener

然后看一个函数setState,我们好好看看这个函数

 1
 2  const setState = (partial, replace) => {
 3  
 4    const nextState =
 5      typeof partial === 'function'
 6        ? partial(state)
 7        : partial
 8       
 9    if (!Object.is(nextState, state)) {
10      const previousState = state
11      state =
12      
13        replace ?? (typeof nextState !== 'object' || nextState === null)
14          ? (nextState as TState)
15          
16          : Object.assign({}, state, nextState)
17       
18      listeners.forEach((listener) => listener(state, previousState))
19    }
20  }
21

我们看到获取了nextState,也很好理解,就是下一个如果parital是个函数就调用,不是就直接赋值,然后判断 nextState,state是否一样,是一个浅层比较,如果一样就证明没有发生更新,直接跳过,这里的state就是之前的 state,因为一直保存在函数闭包里。

然后核心比较如果replace传入true那直接赋值就好,就直接落入了第一个逻辑,否则在看后面的条件 typeof nextState !== ‘object’ || nextState === null 是不是基础类型,如果是同样直接赋值,如果不是就 用Object.assign({}, state, nextState)进行一个浅层合并后赋值,然后再用 listeners 发布订阅消息,使react更新

继续看下面两个函数,获取当前的状态和初始值,值得注意的是initialState在后面被赋值

 1  const getState = () => state
 2
 3  const getInitialState = () =>
 4    initialState

然后就是订阅subscribe和取消订阅,也很简单 subscribe就是把它加入set中然后同时返回一个取消订阅的函数,

destroy在后续会被舍弃,其实差不多就是一个清空的操作

 1  const subscribe = (listener) => {
 2    listeners.add(listener)
 3    
 4    return () => listeners.delete(listener)
 5  }
 6
 7  const destroy = () => {
 8    listeners.clear()
 9  }

最后我们把之前创建的这些函数(setState,getState,subscribe…)作为api返回出去

同时初始化一下state和initialState

 1  const api = { setState, getState, getInitialState, subscribe, destroy }
 2  const initialState = (state = createState(setState, getState, api))
 3  return api

回到之前我们看到我们获取到了api,接着就应该接入react了

 1const createImpl = <T>(createState: StateCreator<T, [], []>) => {
 2
 3  const api =
 4    typeof createState === 'function' ? createStore(createState) : createState
 5
 6  const useBoundStore: any = (selector?: any, equalityFn?: any) =>
 7    useStore(api, selector, equalityFn)
 8
 9  Object.assign(useBoundStore, api)
10
11  return useBoundStore
12}

接入react

useBoundStore核心就是在useStore里让我们看看useStore

 1
 2export function useStore(
 3  api,
 4  selector,
 5  equalityFn,
 6) {
 7  const slice = useSyncExternalStoreWithSelector(
 8    api.subscribe,
 9    api.getState,
10    api.getServerState || api.getInitialState,
11    selector,
12    equalityFn,
13  )
14  return slice
15}

看到其实核心就是用了useSyncExternalStoreWithSelector 这个api,是基于官方的useSyncExternalStore做的一个封装,加上了selector 和 equalityFn,这也是为什么zustand如此简洁的原因之一。

 1const createImpl = (createState) => {
 2  const api =
 3    typeof createState === 'function' ? createStore(createState) : createState
 4
 5  const useBoundStore: any = (selector, equalityFn) =>
 6    useStore(api, selector, equalityFn)
 7
 8   
 9  Object.assign(useBoundStore, api)
10
11  return useBoundStore
12}

最终返回一个函数同时该函数上挂载着各种订阅相关的api

触发更新

我们可以看到create之后我们就可以调用这个useStore了,可以直接取到count和inc在useStore里

 1import { create } from "zustand";
 2
 3const useStore = create((set) => ({
 4  count: 1,
 5  inc: () => set((state) => ({ count: state.count + 1 })),
 6}));
 7
 8export function Counter() {
 9  const { count, inc } = useStore();
10  return (
11    <div>
12      <span>{count}</span>
13      <button onClick={inc}>one up</button>
14    </div>
15  );
16}

点击inc就会触发更新具体会执行之前的

 1const setState: StoreApi = (partial, replace) => {
 2    const nextState =
 3      typeof partial === 'function'
 4        ? partial(state)
 5        : partial
 6    if (!Object.is(nextState, state)) {
 7      const previousState = state
 8      state =
 9        replace ?? (typeof nextState !== 'object' || nextState === null)
10          ? (nextState as TState)
11          : Object.assign({}, state, nextState)
12      listeners.forEach((listener) => listener(state, previousState))
13    }
14  }

需要注意一点的是在调试时发现listeners里会加上一个函数,可以猜测到这个就是触发react更新的关键,大概率是在调用useSyncExternalStoreWithSelector时加上的,由此就接入到react里可以正常更新了,因为有个函数forceStoreRender,不过本文不太深入useSyncExternalStore也就先略过了

使用selector

传入selector,你可选择你需要的state,或者进行一些计算属性

 1import { create } from "zustand";
 2
 3const useStore = create((set) => ({
 4  count: 2,
 5  inc: () => set((state) => ({ count: state.count + 1 })),
 6}));
 7
 8export function Counter() {
 9  const count = useStore((state) => state.count);
10    const keys = useStore((state) => Object.keys(state))
11  return (
12    <div>
13      <span>{count}</span>
14      <button>one up</button>
15    </div>
16  );
17}

这样我们就无需解构就可以直接拿到了count,在源码中zustand做的也很简单就是将selector传给useSyncExternalStoreWithSelector

useShallow

当你需要订阅存储中的一个计算状态时,推荐的方式是使用一个selector

这个计算选择器会在输出发生变化时导致重新渲染,判断变化的方式是使用Object.is。

在这种情况下,你可能希望使用useShallow来避免重新渲染,如果计算出的值始终与先前的值浅相等的话。

一个例子,来自官方文档

 1import { create } from 'zustand'
 2
 3const useMeals = create(() => ({
 4  papaBear: 'large porridge-pot',
 5  mamaBear: 'middle-size porridge pot',
 6  littleBear: 'A little, small, wee pot',
 7}))
 8
 9export const BearNames = () => {
10  const names = useMeals((state) => Object.keys(state))
11
12  return <div>{names.join(', ')}</div>
13}

我们试图更新这个store

 1useMeals.setState({
 2  papaBear: 'a large pizza',
 3})

这个改动导致了BearNames重新渲染,即使根据浅相等的定义,names的实际输出并没有发生变化。

这时候你就可以这样用,来避免重新渲染

 1import { create } from 'zustand'
 2import { useShallow } from 'zustand/react/shallow'
 3
 4const useMeals = create(() => ({
 5  papaBear: 'large porridge-pot',
 6  mamaBear: 'middle-size porridge pot',
 7  littleBear: 'A little, small, wee pot',
 8}))
 9
10export const BearNames = () => {
11  const names = useMeals(useShallow((state) => Object.keys(state)))
12
13  return <div>{names.join(', ')}</div>
14}

看看如何实现的,就是用一个ref去存储之前的值,然后进行比对就是用shallow方法,如果一样就直接返回prevent.current,如果不一样就更新prev.current

 1import { useRef } from 'react'
 2import { shallow } from '../vanilla/shallow.ts'
 3
 4export function useShallow(selector) {
 5  const prev = useRef<U>()
 6
 7  return (state) => {
 8    const next = selector(state)
 9    return shallow(prev.current, next)
10      ? (prev.current)
11      : (prev.current = next)
12  }
13}

然后我们再看看shallow

  • 首先用Object.is判断
  • 排除掉null和基础值,前面已经判断过Object.is不符合说明这些值应该更新
  • 遍历Map,Set一个值一个值进行Object.is比较
  • 如果是普通的Object的就拿到键然后先比长度,长度不相等自然不相等
  • 再遍历对象键值比较,先看有无该键,然后在看该键上值是否相等
 1export function shallow<T>(objA: T, objB: T) {
 2  
 3  if (Object.is(objA, objB)) {
 4    return true
 5  }
 6  
 7  
 8  if (
 9    typeof objA !== 'object' ||
10    objA === null ||
11    typeof objB !== 'object' ||
12    objB === null
13  ) {
14    return false
15  }
16
17  
18  if (objA instanceof Map && objB instanceof Map) {
19    if (objA.size !== objB.size) return false
20
21    for (const [key, value] of objA) {
22      if (!Object.is(value, objB.get(key))) {
23        return false
24      }
25    }
26    return true
27  }
28  
29  if (objA instanceof Set && objB instanceof Set) {
30    if (objA.size !== objB.size) return false
31
32    for (const value of objA) {
33      if (!objB.has(value)) {
34        return false
35      }
36    }
37    return true
38  }
39
40  
41  const keysA = Object.keys(objA)
42  if (keysA.length !== Object.keys(objB).length) {
43    return false
44  }
45  for (let i = 0; i < keysA.length; i++) {
46    if (
47      
48      !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
49      
50      !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
51    ) {
52      return false
53    }
54  }
55  return true
56}

中间件

zustand有几个很好的中间件,我就先只带大家看个immer的,其他大家可以自行查看

我们先要下载一下 immer, 然后再从zustand/middleware/immer中引入immer

 1import { create } from "zustand";
 2import { immer } from "zustand/middleware/immer";
 3
 4type State = {
 5  count: number;
 6};
 7
 8type Actions = {
 9  increment: (qty: number) => void;
10  decrement: (qty: number) => void;
11};
12
13const useCountStore = create<State & Actions>()(
14  immer((set) => ({
15    count: 0,
16    increment: (qty: number) =>
17      set((state) => {
18        state.count += qty;
19      }),
20    decrement: (qty: number) =>
21      set((state) => {
22        state.count -= qty;
23      }),
24  }))
25);
26export function Counter() {
27  const { count, increment } = useCountStore();
28  return (
29    <div>
30      <span>{count}</span>
31      <button
32        onClick={() => {
33          increment(2);
34        }}
35      >
36        two up
37      </button>
38    </div>
39  );
40}

我们来看源码看出导出的这个就是个immerImpl

 1const immerImpl = (initializer) => {
 2  return (set, get, store) => {
 3      store.setState = (updater, replace, ...a) => {
 4        
 5        const nextState = (
 6          typeof updater === 'function' ? produce(updater) : updater
 7        )
 8          return set(nextState, replace, ...a)
 9      }
10
11    return initializer(store.setState, get, store)
12  }
13}

其实就是相当于代理了之前的setState,在之前setState之前用immer的produce处理了nextState,然后正常再set就是之前setState

处理异步

在zustand里我们可以很容易处理异步,几乎无感

 1import { create } from "zustand";
 2
 3const useCountStore = create((set) => ({
 4  data: {},
 5  fetch: async () => {
 6    const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
 7    const result = await res.json();
 8    set({ data: result });
 9  },
10}));
11export function Counter() {
12  const { data, fetch } = useCountStore();
13  return (
14    <div>
15      <span>{data.id}</span>
16      <button
17        onClick={() => {
18          fetch();
19        }}
20      >
21        fetch
22      </button>
23    </div>
24  );
25}

总结

zustand的设计足够简单,十分灵活,代码也很简洁,充分利用了react的hook,中间件的设计增强了该库的拓展性,十分建议大家通过此文自己去看看zustand的源代码,本文只起一个抛砖引玉的作用。

附一张网络上的原理图,不是我画的但觉得蛮好的

个人笔记记录 2021 ~ 2025