Tuntun Blog

手写zustand

阅读时间: 4分钟 21秒

介绍

Zustand是一个开源的React状态库,可以作为Redux的替代品。具有比redux更简洁的语法和更灵活的使用方法

两者对比: zustand-vs-redux

实现

 
const createStore = (createState) => {
  let state // store内部状态存储于state上
  const getStore = () => state
  const setStore = () => {} // setState就是create接收函数的入参
  const subscribe = () => {} // 每次订阅时将subscribe加入到listeners,subscribe的作用是触发组件重新渲染
  const api = { getStore, setStore, subscribe }
  state = createState(setStore) // state的初始值就是createState的调用结果
  return api
}
 
const useStore = (api, selector, equalityFn) => {}
 
export const create = (createState) => {
  const api = createStore(createState) // 拿到store,包含了全部操作store的方法
  const useBoundStore = (selector, equalityFn) =>
    useStore(api, selector, equalityFn)
  return useBoundStore
}
  • createStore 用来创建 Store,内部维护了 Store 的状态以及操作 Store 的函数 API,其中包括了:
    • 获取状态函数 getStore;
    • 设置状态函数 setStore;
  • 订阅函数 subscribe,组件会订阅这个 Store,当状态发生改变时会重新渲染。
  • useBoundStore 接收 selector(从完整的状态中选取部分状态),equalityFn(用来对比选取状态是否发生变化,从而决定是否重新渲染)。
  • useStore 借助 useSyncExternalStoreWithSelector 完成订阅、状态选取、re-render 优化,返回选取的状态。
  • create 完成上述函数的组合。

其中通知react状态更新使用的api是useSyncExternalStoreWithSelector,其底层是useSyncExternalStore

最终代码:

import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector' // 内部是useSyncExternalStore,useSyncExternalStoreWithSelector在其基础上增加了memorized,用selector防止re-render
 
type Subscribe = Parameters<typeof useSyncExternalStoreWithSelector>[0]
 
type GetState<T> = () => T
 
type SetState<T> = (
  partial: T | Partial<T> | ((state: T) => T | Partial<T>)
) => void
 
type StoreApi<T> = {
  setState: SetState<T>
  getState: GetState<T>
  subscribe: Subscribe
}
 
type StateCreator<T> = (setState: SetState<T>) => T
 
type EqualityFn<T> = (a: T, b: T) => boolean
 
/**
 * `createStore` 用来创建Store
 */
const createStore = <T>(createState: StateCreator<T>): StoreApi<T> => {
  const listeners = new Set<(state: T) => void>()
  let state: T // store内部状态存储于state上
  const setState: SetState<T> = (partial) => {
    // setState就是create接收函数的入参
    const nextState =
      typeof partial === 'function'
        ? (partial as (state: T) => T)(state)
        : partial
    if (!Object.is(nextState, state)) {
      state =
        typeof nextState !== 'object' || nextState === null
          ? (nextState as T)
          : Object.assign({}, state, nextState)
      // 当状态发生变化时,依次通知组件re-render,也就是循环调用一遍listeners的所有函数
      listeners.forEach((listener) => listener(state))
    }
  }
  const getState = () => state
  const subscribe: Subscribe = (subscribe) => {
    // 每次订阅时将subscribe加入到listeners,subscribe的作用是触发组件重新渲染
    listeners.add(subscribe)
    return () => listeners.delete(subscribe)
  }
  const api = { setState, getState, subscribe }
  state = createState(setState) // state的初始值就是createState的调用结果
  return api
}
 
/**
 * `useStore` 借助 `useSyncExternalStoreWithSelector` 完成订阅、状态选取、re-render优化,返回选取的状态
 */
const useStore = <State, StateSlice>(
  api: StoreApi<State>,
  selector: (state: State) => StateSlice = api.getState as any,
  equalityFn?: EqualityFn<StateSlice>
) => {
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getState,
    selector,
    equalityFn
  )
  return slice
}
 
export const create = <T>(createState: StateCreator<T>) => {
  const api = createStore(createState) // 拿到store,包含了全部操作store的方法
  const useBoundStore = <TSlice = T>(
    selector?: (state: T) => TSlice,
    equalityFn?: EqualityFn<TSlice>
  ) => useStore<T, TSlice>(api, selector, equalityFn)
  Object.assign(useBoundStore, api)
  return useBoundStore as typeof useBoundStore & StoreApi<T>
}

使用

import { create } from './my-zustand'
 
export interface TodoItem {
  id: string
  todo: string
  status: string
}
 
interface TodoState {
  todoList: TodoItem[]
}
 
interface TodoAction {
  setTodoList: (fn: (list: TodoItem[]) => TodoItem[]) => void
}
 
export const useTodoStore = create<TodoState>(() => ({
  todoList: [] as TodoItem[],
  // setTodoList(fn) {
  //   set((prev) => ({ todoList: fn(prev.todoList) }))
  // },
}))
 
export const setTodoList = (fn: (list: TodoItem[]) => TodoItem[]) => {
  useTodoStore.setState((prev) => ({ todoList: fn(prev.todoList) }))
}
 
// setInterval(() => {
//   setTodoList(
//     produce((list) => {
//       list.pop()
//     })
//   )
// }, 3000)