跳转至

Walkthrough | 手写 60 行 Store(阶段 3 练习答案)

对应练习phase-03-state.md 3.11 练习任务 1 配套测试practice-tests/src/store.test.ts


目标

不看任何参考,手写一个 60 行的 Store,支持: 1. getState() 返回当前 state 2. setState(updater) 更新 state 3. subscribe(listener) 订阅变化 4. 引用相等时跳过通知Object.is 优化) 5. onChange 同步副作用钩子

步骤 1:理解需求

参考 Claude Code 真实使用: - 状态变更 → 同步触发 onChange(用于持久化) - 状态变更 → 通知所有 listeners - Object.is(next, prev) 跳过不必要的更新

步骤 2:思考 5 分钟再写

闭眼想清楚: - 用什么数据结构存 state?let state - 怎么存 listeners?Set<Listener>(去重) - setState 的流程?updater → Object.is 检查 → onChange → 通知 listeners - subscribe 的返回值?unsubscribe 函数

步骤 3:写代码(30 分钟)

// 第 1-5 行:类型定义
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void

export type Store<T> = {
  getState: () => T
  setState: (updater: (prev: T) => T) => void
  subscribe: (listener: Listener) => () => void
}

// 第 6-15 行:工厂函数签名
export function createStore<T>(
  initialState: T,
  onChange?: OnChange<T>,
): Store<T> {
  // 第 16-17 行:闭包状态
  let state = initialState
  const listeners = new Set<Listener>()

  // 第 18-19 行:getState
  return {
    getState: () => state,

    // 第 20-28 行:setState
    setState: (updater) => {
      const prev = state
      const next = updater(prev)
      if (Object.is(next, prev)) return  // 引用相等跳过
      state = next
      onChange?.({ newState: next, oldState: prev })  // 同步副作用
      for (const listener of listeners) listener()  // 通知
    },

    // 第 29-32 行:subscribe
    subscribe: (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}

步骤 4:测试(30 分钟)

13 个测试覆盖:

// 1. getState 返回初始
it('1. getState returns initial state', () => {
  const store = createStore({ count: 0 })
  expect(store.getState()).toEqual({ count: 0 })
})

// 2. setState 触发监听器
it('2. setState triggers listeners', () => {
  const store = createStore({ count: 0 })
  const listener = vi.fn()
  store.subscribe(listener)

  store.setState(prev => ({ count: prev.count + 1 }))

  expect(listener).toHaveBeenCalledTimes(1)
  expect(store.getState()).toEqual({ count: 1 })
})

// 3. 引用相等时**不**触发
it('3. setState with same reference does NOT trigger listeners', () => {
  const state = { count: 0 }
  const store = createStore(state)
  const listener = vi.fn()
  store.subscribe(listener)

  store.setState(prev => prev)  // 返回相同引用

  expect(listener).not.toHaveBeenCalled()
  expect(store.getState()).toBe(state)
})

// 4. unsubscribe 有效
it('4. subscribe returns unsubscribe function', () => {
  const store = createStore({ count: 0 })
  const listener = vi.fn()
  const unsubscribe = store.subscribe(listener)

  store.setState(prev => ({ count: prev.count + 1 }))
  expect(listener).toHaveBeenCalledTimes(1)

  unsubscribe()

  store.setState(prev => ({ count: prev.count + 1 }))
  expect(listener).toHaveBeenCalledTimes(1)  // 仍为 1
})

// 5. onChange 同步触发,**在 listeners 之前**
it('5. onChange fires synchronously, BEFORE listeners', () => {
  const callOrder: string[] = []
  const store = createStore(
    { count: 0 },
    (args) => {
      callOrder.push('onChange')
      callOrder.push(`onChange:newState=${args.newState.count}`)
      callOrder.push(`onChange:oldState=${args.oldState.count}`)
    },
  )
  store.subscribe(() => {
    callOrder.push('listener')
  })

  store.setState(prev => ({ count: prev.count + 1 }))

  expect(callOrder).toEqual([
    'onChange',
    'onChange:newState=1',
    'onChange:oldState=0',
    'listener',
  ])
})

// 6-13: bonus 测试
// 6. 多个 listeners 都触发
// 7. spread 保留未变字段引用
// 8. 重复订阅同一 listener 只算一次(Set 语义)
// 9. listener 内 subscribe 的新 listener 在本轮也被通知(Set 迭代器反映当前内容)
// 10. listener 抛错时冒泡
// 11. onChange 收到 new + old
// 12. onChange 引用相等时不触发
// 13. onChange 用于持久化

步骤 5:跑测试验证

cd learn_doc/practice-tests
bun test
# 期望:13 pass / 0 fail

常见错误

错误 1:用 Map 而不是 Set

// ❌ 错的
const listeners = new Map<string, Listener>()

// ✅ 对的
const listeners = new Set<Listener>()

原因:Set 自动去重(同一 listener 多次 subscribe 只算一次)。Map 还需要 key。

错误 2:onChange 在 listeners 之后触发

// ❌ 错的
setState: (updater) => {
  const prev = state
  const next = updater(prev)
  if (Object.is(next, prev)) return
  state = next
  for (const listener of listeners) listener()  // listeners 先
  onChange?.({ newState: next, oldState: prev })  // onChange 后
}

// ✅ 对的
setState: (updater) => {
  const prev = state
  const next = updater(prev)
  if (Object.is(next, prev)) return
  state = next
  onChange?.({ newState: next, oldState: prev })  // onChange 先
  for (const listener of listeners) listener()  // listeners 后
}

原因:onChange 用于持久化时,listeners 应该看到"已经持久化"的 state。反过来,listeners 在 onChange 之前触发,会读到"还没持久化"的 state。

错误 3:for-of 集合类型选错

// ❌ 错的:用 Array
const listeners: Listener[] = []

// ✅ 对的:用 Set
const listeners = new Set<Listener>()

原因: 1. Set 自动去重 2. for...of 在 Set 迭代中新增的元素会被访问到(这是 Claude Code 真实行为)

错误 4:subscribe 漏 return unsubscribe

// ❌ 错的
subscribe: (listener) => {
  listeners.add(listener)
  // 漏 return
}

// ✅ 对的
subscribe: (listener) => {
  listeners.add(listener)
  return () => listeners.delete(listener)
}

进阶:和 Zustand 对比

Zustand v4 的 store.ts(精简):

// zustand/vanilla.ts
const createStore = (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
      state = (replace ?? (typeof nextState !== 'object' || nextState === null))
        ? nextState
        : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

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

对比 Claude Code: - 几乎一模一样(Object.is 检查 + listeners Set + unsubscribe) - 区别:Zustand 支持浅 mergeObject.assign({}, state, nextState)),Claude Code 强制全替换 - Zustand 多了 subscribeWithSelector / persist / devtools 中间件

意义:Claude Code 的 store 是 Zustand 风格的"最小子集"。

关键洞察

1. 60 行 = 完整状态管理

没有 Redux Toolkit 也能做大型项目
没有中间件也能管理复杂副作用(onChange 一个钩子够用)。

2. Object.is 是性能核心

没有 Object.is 检查,每次 setState 都会触发所有 listeners
有了它,只在真正变化时通知

3. Set + for-of 的"边迭代边修改"语义

for...of 迭代 Set 时反映当前内容(包括本轮 add 的新元素)。
Claude Code 真实实现就是这行为。

4. onChange 比 React useEffect 更可控

  • useEffect 异步(不阻塞 setState)
  • onChange 同步(在 setState 内)
  • 同步副作用(如持久化)用 onChange 更安全

实战用法

// 1. 创建
const appStore = createStore<AppState>(createAppState(), (args) => {
  // 同步持久化
  saveToDisk(args.newState)
})

// 2. 订阅
const unsubscribe = appStore.subscribe(() => {
  console.log('state changed:', appStore.getState())
})

// 3. 更新
appStore.setState(prev => ({ ...prev, isLoading: true }))

// 4. 读取
const current = appStore.getState()

// 5. 取消订阅
unsubscribe()

配套资源

  • Claude Code 真实源码src/state/store.ts(60 行)
  • 配套测试learn_doc/practice-tests/src/store.test.ts(13 测试)
  • API 速查reference/api-quickref.md 第 1 节