如何用 30 行代码手写一个支持选择器(Selector)与细粒度重渲染控制的 Zustand 状态库?

Zustand 的源码极其精炼,其核心架构由两部分组成:1. 与框架无关的 Vanilla 发布订阅状态容器(createStore);2. 基于 React 18 原生 useSyncExternalStore 桥接并支持 selector 切片比对的 React Hook(create)。

一、 完整手写实现(含 Vanilla 核心 + React Hook 桥接 + 浅比较优化)

import { useSyncExternalStore } from 'react'

// 第一层:与 React 完全解耦的纯 JS 发布订阅状态机(可在任意 JS/TS 工具函数中直接调用)
export function createStore(createState) {
  let state
  const listeners = new Set()

  const getState = () => state

  const setState = (partial, replace = false) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial
    // 使用 Object.is 拦截相同引用更新
    if (!Object.is(nextState, state)) {
      const previousState = state
      state = replace ? nextState : Object.assign({}, state, nextState)
      // 通知所有订阅者并传入新旧状态
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener) // 返回优雅退订函数
  }

  // 初始化状态:将 setState、getState 注入给用户的定义函数
  state = createState(setState, getState)
  return { getState, setState, subscribe }
}

// 第二层:桥接 React 并发渲染安全原语 useSyncExternalStore
const identity = (s) => s

export function create(createState) {
  const api = createStore(createState)

  const useBoundStore = (selector = identity) => {
    return useSyncExternalStore(
      api.subscribe,
      () => selector(api.getState()), // 客户端读取切片快照(React 内部自动用 Object.is 比对切片返回值决定是否重渲染!)
      () => selector(api.getState())  // SSR 服务端渲染读取初始快照
    )
  }

  // 将 getState / setState / subscribe 静态方法直接挂载到 Hook 函数上,支持组件外调用
  Object.assign(useBoundStore, api)
  return useBoundStore
}

二、 面试深挖:当 Selector 返回新对象时为什么会无限重渲染?如何用 useShallow 解决?

在 React 18+ 中,useSyncExternalStore 会对 getSnapshot(即 () => selector(api.getState()))的返回值做严格的 Object.is(prevSlice, nextSlice) 比对。如果用户写成:
const { a, b } = useStore(s => ({ a: s.a, b: s.b }))
由于每次调用 selector 都返回一个全新的对象字面量引用 {},Object.is 永远为 false,会立即触发 React 报错 Maximum update depth exceeded!

// 手写 Zustand 必备伴侣:useShallow 浅比较包装器(当新旧对象第一层属性值未变时复用上一次的对象引用)
import { useRef } from 'react'

export function useShallow(selector) {
  const prev = useRef()
  return (state) => {
    const next = selector(state)
    if (prev.current && shallowEqual(prev.current, next)) {
      return prev.current // 关键:属性浅相等时返回上一次缓存的旧引用,让 useSyncExternalStore 的 Object.is 通过!
    }
    prev.current = next
    return next
  }
}

同分类其他题目