手写:简易状态管理库深度解析
30 行 createStore → 中间件链 → Proxy 响应式 → 完整状态管理库,一步步拆解状态管理的底层原理。
一句话概括
所有现代状态管理库的骨架都是同一件事:“存状态 + 改状态 + 通知变化”——Redux 用 reducer + dispatch + subscribe,Vuex 用 Proxy + commit + watch,Zustand 用闭包 + set + selector,骨架一致,皮肉不同。
核心知识点
1. 20 行 createStore —— 最简骨架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createStore(reducer, initialState) {
let state = initialState
const listeners = new Set()
return {
getState: () => state,
dispatch: (action) => {
state = reducer(state, action)
listeners.forEach(fn => fn())
return action
},
subscribe: (fn) => {
listeners.add(fn)
return () => listeners.delete(fn)
},
}
}
// 使用
const reducer = (state = 0, { type }) =>
type === 'ADD' ? state + 1 : state
const store = createStore(reducer, 0)
store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'ADD' }) // 打印 1
三句话总结:getState 读,dispatch 写(通过 reducer),subscribe 订阅变化。没有黑魔法。
2. combineReducers —— 怎么合并多个 reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
function combineReducers(reducers) {
return (state = {}, action) => {
const next = {}
let changed = false
for (const key of Object.keys(reducers)) {
const prev = state[key]
next[key] = reducers[key](prev, action)
if (next[key] !== prev) changed = true
}
// 关键优化:没变化就返回同一个引用,React 用 === 跳过渲染
return changed ? next : state
}
}
面试常问:”combineReducers 为什么遍历所有 reducer?”——因为 Redux 不知道这个 action 被哪个 reducer 消费,所以全调一遍,谁不处理就返回原 state。
3. 中间件链 —— compose 闭包嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// compose(f, g, h) → (...args) => f(g(h(...args)))
function compose(...funcs) {
if (funcs.length === 0) return arg => arg
if (funcs.length === 1) return funcs[0]
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
// 中间件的标准签名:(store) => (next) => (action) => {}
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
const chain = middlewares.map(m => m({
getState: store.getState,
dispatch: (action) => dispatch(action), // 稍后会被覆盖
}))
let dispatch = compose(...chain)(store.dispatch)
return { ...store, dispatch }
}
}
// 手写 thunk 中间件:6 行代码
const thunk = ({ dispatch, getState }) => (next) => (action) =>
typeof action === 'function'
? action(dispatch, getState) // 拦截函数式 action
: next(action) // 普通 action 放行
// 手写 logger 中间件
const logger = ({ getState }) => (next) => (action) => {
console.log('prev:', getState())
const result = next(action)
console.log('next:', getState())
return result
}
核心理解:中间件的三层柯里化——第一层注入 store API,第二层拿到下一个中间件的 dispatch,第三层处理当前 action。洋葱模型就是 compose 的从右到左嵌套。
4. Proxy 响应式 —— 依赖收集 + 派发更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function reactive(target) {
const deps = new Map() // key → Set<callback>
return new Proxy(target, {
get(obj, key) {
if (activeEffect) {
if (!deps.has(key)) deps.set(key, new Set())
deps.get(key).add(activeEffect)
}
const val = Reflect.get(obj, key)
// 懒递归:只在访问时才代理嵌套对象
return typeof val === 'object' && val !== null ? reactive(val) : val
},
set(obj, key, val) {
const old = Reflect.get(obj, key)
const ok = Reflect.set(obj, key, val)
if (old !== val) deps.get(key)?.forEach(fn => fn())
return ok
},
})
}
let activeEffect = null
function watchEffect(fn) {
activeEffect = fn
fn() // 首次执行,触发 get → 收集依赖
activeEffect = null
}
// 使用
const state = reactive({ count: 0, user: { name: 'Alice' } })
watchEffect(() => console.log('count changed:', state.count))
state.count++ // 打印: count changed: 1
Proxy vs Object.defineProperty 面试必问:Proxy 不侵入原始对象、能拦截新增/删除属性、原生支持数组操作;defineProperty 需要递归遍历所有 key、无法检测 arr[0] = x、需要 $set 补丁。
5. useSelector 怎么知道要不要重渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// React 18 的 useSyncExternalStore 让这事变得极其简单
import { useSyncExternalStore } from 'react'
function useSelector(selector) {
const store = useContext(StoreContext)
return useSyncExternalStore(
store.subscribe, // store 变了就通知 React
() => selector(store.getState()) // 每次读取最新的选中值
)
}
// 不在组件内手动 setState——useSyncExternalStore 内部处理了:
// 1. subscribe 回调触发时会检查 selector 返回值
// 2. 新旧值用 Object.is 比较
// 3. 不同才安排重渲染
React 17 及以前,React-Redux 用 forceUpdate + 手动对比实现;React 18 的 useSyncExternalStore 把这个模式标准化了,并发模式下也不会出现撕裂(tearing)。
其实你每天都在用
- Redux DevTools 就是状态管理的终极应用 — 它通过 enhancer 劫持了 store 的 dispatch,记录每一次 state 快照,实现时间旅行。原理就是本文的
createStore+ 历史记录数组 - Vue 的
reactive()就是上面 Proxy 的加强版 — 加上Reflect处理 this 绑定问题、处理数组 length 的特殊情况、处理 Map/Set 的代理 - React 的
useReducer就是内建版的 createStore —const [state, dispatch] = useReducer(reducer, init),只是订阅者变成了 React 自身的渲染调度 - Node.js 的 EventEmitter 就是最简状态管理 —
emitter.on('change', fn)+emitter.emit('change', data),跟 store.subscribe 一个意思 - 浏览器
window.postMessage/BroadcastChannel就是跨标签页的 subscribe — 一个标签页 dispatch action,其他标签页收到消息更新状态
常见误解(FAQ)
❌ 误区:「Redux 的 store 是一个全局单例对象」 Redux 的 createStore 每次调用都返回独立实例,根本不是单例。”全局单例”的印象来自大多数项目只调一次 createStore,但这只是习惯,不是约束——微前端里一个子应用一个 store 完全可行。
❌ 误区:「dispatch 是同步的,所以 Redux 不支持异步」 dispatch 本身确实同步执行 reducer 然后同步通知所有 listener。但”支持异步”靠的是中间件——thunk/promise/redux-saga 在 action 到达 reducer 之前拦截它,先执行异步逻辑,异步完成后再 dispatch 一个普通 action。这里的关键区别:dispatch 同步,但中间件让 action 的来源可以是异步的。
❌ 误区:「subscribe 后组件一定会重新渲染」 subscribe 只是通知”状态变了”,具体要不要渲染是 React-Redux 的 useSelector 决定的——它拿到状态变化通知后,重新跑 selector,只有 selector 返回值变了(Object.is 比较)才会触发渲染。这也是为什么 selector 里不能每次都返回新对象,否则组件每次都重渲染。
❌ 误区:「Object.defineProperty 已经能实现响应式了,Proxy 只是语法糖」 这是最危险的误解。defineProperty 的硬伤无法通过任何 workaround 修补:无法检测新增/删除属性、数组索引赋值不触发 setter、必须递归遍历所有属性(成本 O(n))。Proxy 是语言层面的代理,拦截 13 种操作,arr[0] = 1、obj.newKey = 1、delete obj.oldKey 全能拦截。
一句话总结
状态管理库的本质就三个词——闭包存状态、观察者通知变化、中间件扩展能力,想清楚这三个怎么交互,Redux/Vuex/Zustand/Pinia/MobX 的内部实现全都能推导出来。