文章

手写:简易状态管理库深度解析

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)。

其实你每天都在用

  1. Redux DevTools 就是状态管理的终极应用 — 它通过 enhancer 劫持了 store 的 dispatch,记录每一次 state 快照,实现时间旅行。原理就是本文的 createStore + 历史记录数组
  2. Vue 的 reactive() 就是上面 Proxy 的加强版 — 加上 Reflect 处理 this 绑定问题、处理数组 length 的特殊情况、处理 Map/Set 的代理
  3. React 的 useReducer 就是内建版的 createStore — const [state, dispatch] = useReducer(reducer, init),只是订阅者变成了 React 自身的渲染调度
  4. Node.js 的 EventEmitter 就是最简状态管理 — emitter.on('change', fn) + emitter.emit('change', data),跟 store.subscribe 一个意思
  5. 浏览器 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 的内部实现全都能推导出来。

本文由作者按照 CC BY 4.0 进行授权