Last updated on

Zustand 核心能力与代码梳理

https://github.com/sedationh/debug-zustand

用法分析

import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))
function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

create 传入函数,传入的函数「A」可以拿到 set 的方法用于更改状态,A 返回一个 hooks 「B」,B 可以传入 selector 来按需引用,减少渲染,B 返回 所选的状态

入口

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

跳转代码 src/react.ts

export const create = (<T>(
  createState: StateCreator<T, [], []> | undefined
) => {
  // STUDY: seda 入口函数
  return createState ? createImpl(createState) : createImpl
}) as Create

进入 createImpl

import { createStore } from './vanilla.ts'

const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  if (
    import.meta.env?.MODE !== 'production' &&
    typeof createState !== 'function'
  ) {
    console.warn(
      "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`."
    )
  }
  const api =
    typeof createState === 'function' ? createStore(createState) : createState
...

进入 createStore src/vanilla.ts

export const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore


const createStoreImpl: CreateStoreImpl = (createState) => {
  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) => {

可见核心 能力是由 vanilla.ts 完成的,对 react 的场景在 vanilla 上包一层 另外,源代码中半数以上的代码都是在写类型,如下图

image

考虑到这里处理的 TS 场景还比较复杂,本节不予考虑,后面单开一节写

vanilla

https://www.unpkg.com/browse/zustand@4.4.0/esm/vanilla.mjs 从官方的 mjs 版本去除一些非核心分支和判断可得下述代码

const createStoreImpl = (createState) => {
  let state
  const listeners = new Set()

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial

    if (!Object.is(nextState, state)) {
      const previousState = state
      // https://docs.pmnd.rs/zustand/guides/immutable-state-and-merging#replace-flag
      // However, as this is a common pattern, set actually merges state, and we can skip the ...state part:
      state =
        replace ?? typeof nextState !== 'object'
          ? nextState
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const getState = () => state

  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  const destroy = () => listeners.clear()

  const api = { setState, getState, subscribe, destroy }

  state = createState(setState, getState, api)

  return api
}
const createStore = (createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl

export { createStore }

实现了一个监听和通知 用法如下,可见我们需要去手动的注册监听、取消监听、维持状态并触发更新

const vStore = createStore((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

function VanillaPage() {
  const [v, setV] = useState(() => vStore.getState())
  useEffect(() => {
    return vStore.subscribe((state) => {
      setV(state)
    })
  }, [])

  return (
    <div>
      VanillaPage
      <button
        onClick={() => {
          v.increase()
        }}>
        increase
      </button>
      {v.count}
    </div>
  )
}

react 提供一个用于订阅的 hooks — useSyncExternalStore,上面的代码可被改写为

const vStore = createStore((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

function SyncExternalStore() {
  const v = useSyncExternalStore(vStore.subscribe, vStore.getState)
  return (
    <div>
      SyncExternalStore
      <button
        onClick={() => {
          v.increase()
        }}>
        increase
      </button>
      {v.count}
    </div>
  )
}

react

结合上面的 react useSyncExternalStore 使用场景,可以理解下面的 react 实现了

import { useDebugValue } from 'react'
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector.js'
import { createStore } from './vanilla.js'

const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports

function useStore(api, selector = api.getState, equalityFn) {
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  )
  useDebugValue(slice)
  return slice
}

const createImpl = (createState) => {
  const api = createStore(createState)

  const useBoundStore = (selector, equalityFn) =>
    useStore(api, selector, equalityFn)

  return useBoundStore
}

const create = (createState) =>
  createState ? createImpl(createState) : createImpl

export { create }

useSyncExternalStoreWithSelector 的前三个入参和 useSyncExternalStore 一样 官方的相关讨论 https://github.com/reactwg/react-18/discussions/86 zustand 的的修改 https://github.com/pmndrs/zustand/pull/550/files?short_path=7ae45ad#diff-ca56e63fa839455c920562a44ebc44594f47957bbd3e9873c8a9e64104af2c41L103

之前做 强制渲染的写法是

const [, forceUpdate] = useReducer((c) => c + 1, 0)

useSyncExternalStoreWithSelector 也是在 useSyncExternalStore 上进行的 memo selector 封装

image

selector 的能力并不是 zustand 实现的,而是交给了 react 去比对 selection 「selector 产生的 state」是否变化来决定是让组件进行 forceUpdate

middleware

如何添加一个 middleware 以 immer 为例子

import { immer } from '../mini-js/immer.js'
import { create } from '../mini-js/react.js'

export const useTodoStore = create(
  immer((set) => ({
    todos: {
      '82471c5f-4207-4b1d-abcb-b98547e01a3e': {
        id: '82471c5f-4207-4b1d-abcb-b98547e01a3e',
        title: 'Learn Zustand',
        done: false,
      },
      '354ee16c-bfdd-44d3-afa9-e93679bda367': {
        id: '354ee16c-bfdd-44d3-afa9-e93679bda367',
        title: 'Learn Jotai',
        done: false,
      },
      '771c85c5-46ea-4a11-8fed-36cc2c7be344': {
        id: '771c85c5-46ea-4a11-8fed-36cc2c7be344',
        title: 'Learn Valtio',
        done: false,
      },
      '363a4bac-083f-47f7-a0a2-aeeee153a99c': {
        id: '363a4bac-083f-47f7-a0a2-aeeee153a99c',
        title: 'Learn Signals',
        done: false,
      },
    },
    toggleTodo: (todoId) =>
      set((state) => {
        state.todos[todoId].done = !state.todos[todoId].done
      }),
  }))
)

export default () => {
  const todo = useTodoStore()
  return (
    <div>
      ImmerPage
      <h1>{JSON.stringify(todo.todos)}</h1>
      <button
        onClick={() => todo.toggleTodo('82471c5f-4207-4b1d-abcb-b98547e01a3e')}>
        toggleTodo
      </button>
    </div>
  )
}
const immer = (initializer) => (set, get, store) => {
  store.setState = (updater, replace, ...a) => {
    const nextState = typeof updater === 'function' ? produce(updater) : updater
    return set(nextState, replace, ...a)
  }
  return initializer(store.setState, get, store)
}

export { immer }

把传入的 set 方法包了一层