https://github.com/sedationh/debug-zustand
JavaScriptimport { create } from 'zustand' const useStore = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), }))
React JSXfunction 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 返回 所选的状态
TypeScriptimport { create } from 'zustand' const useBearStore = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), }))
跳转代码 src/react.ts
TypeScriptexport const create = (<T>( createState: StateCreator<T, [], []> | undefined ) => { // STUDY: seda 入口函数 return createState ? createImpl(createState) : createImpl }) as Create
进入 createImpl
TypeScriptimport { 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
TypeScriptexport 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 上包一层 另外,源代码中半数以上的代码都是在写类型,如下图
考虑到这里处理的 TS 场景还比较复杂,本节不予考虑,后面单开一节写
https://www.unpkg.com/browse/zustand@4.4.0/esm/vanilla.mjs 从官方的 mjs 版本去除一些非核心分支和判断可得下述代码
JavaScriptconst 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 }
实现了一个监听和通知 用法如下,可见我们需要去手动的注册监听、取消监听、维持状态并触发更新
React JSXconst 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,上面的代码可被改写为
React JSXconst 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 useSyncExternalStore 使用场景,可以理解下面的 react 实现了
React JSXimport { 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
之前做 强制渲染的写法是
JavaScriptconst [, forceUpdate] = useReducer((c) => c + 1, 0)
useSyncExternalStoreWithSelector 也是在 useSyncExternalStore 上进行的 memo selector 封装
selector 的能力并不是 zustand 实现的,而是交给了 react 去比对 selection 「selector 产生的 state」是否变化来决定是让组件进行 forceUpdate
如何添加一个 middleware 以 immer 为例子
React JSXimport { 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> ) }
React JSXconst 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 方法包了一层