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:跑测试验证¶
常见错误¶
错误 1:用 Map 而不是 Set¶
原因: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 支持浅 merge(Object.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 节