手写一个前端状态管理库,深入理解响应式原理

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-06 22:02 ·2 浏览 ·0 回复

当面试官让我手写状态管理库

前端框架千千万,状态管理却总是那几招:Redux 的单一数据源,MobX 的响应式代理,Vuex 的集中式存储。背过源码解读,刷过原理文章,可真到面试官递来一张白纸:“手写一个简易状态管理库”——不少人瞬间卡壳。
其实响应式原理没那么玄乎,拆开揉碎,无非是“收集依赖 + 触发更新”八个字。今天我们不依赖任何框架,用原生 JavaScript 一步步实现一个支持响应式的迷你状态库,顺便把依赖追踪、订阅发布这些概念彻底吃透。

从最简单的发布订阅开始

状态管理的核心是“当数据变化时,通知所有关心这份数据的人”。最简单粗暴的实现就是发布订阅模式:

class SimpleStore {
  constructor(state) {
    this.state = state
    this.listeners = []
  }
  setState(partial) {
    Object.assign(this.state, partial)
    this.listeners.forEach(fn => fn(this.state))
  }
  subscribe(fn) {
    this.listeners.push(fn)
    return () => {
      const idx = this.listeners.indexOf(fn)
      if (idx > -1) this.listeners.splice(idx, 1)
    }
  }
}

用起来没问题,但它有一个致命弱点:不管改哪个字段,所有订阅者都会收到通知。假如组件 A 只关心 `user.name`,组件 B 只关心 `list`,改个名字却让 B 也重新渲染,效率就低了。

真正响应式:依赖收集

我们希望的效果是:数据被“读取”时记录读取者,数据被“修改”时只通知对应的读取者。这就是 Vue 2 / MobX 响应式的核心思路。

定义两个角色:

- 依赖(Dep):每个响应式字段都对应一个 Dep,Dep 里存放所有“读取过这个字段”的副作用函数(比如渲染函数)。
- 观察者(Watcher / Effect):一段会读取响应式数据的函数。在它执行期间,所有被读取的字段会把该函数收集进自己的 Dep。

JavaScript 的 `Object.defineProperty` 或 Proxy 能帮我们拦截属性的读写。先看一个基于 Proxy 的极简实现:

class Dep {
  constructor() {
    this.subs = new Set()
  }
  depend() {
    if (activeEffect) this.subs.add(activeEffect)
  }
  notify() {
    this.subs.forEach(effect => effect())
  }
}

let activeEffect = null

function effect(fn) {
  const wrapper = () => {
    activeEffect = wrapper
    fn()
    activeEffect = null
  }
  wrapper()
  return wrapper
}

function reactive(obj) {
  const deps = new Map() // key -> Dep
  return new Proxy(obj, {
    get(target, key) {
      const dep = getDep(target, key, deps)
      dep.depend() // 收集当前正在执行的 effect
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value)
      const dep = getDep(target, key, deps)
      dep.notify() // 触发依赖更新
      return result
    }
  })
}

function getDep(target, key, deps) {
  if (!deps.has(key)) deps.set(key, new Dep())
  return deps.get(key)
}

别小看这几十行代码,它已经是一个完整的“迷你响应式系统”了。试试看:

const user = reactive({ name: '张三', age: 30 })
effect(() => {
  console.log('名字变化:', user.name)
})
user.name = '李四' // 打印:名字变化
user.age = 31     // 不打印,因为 effect 没有读取 age

完美做到精确更新。

进阶:支持 Vuex 风格的 commit 与 actions

Vuex 和 Redux 比 MobX 多了“状态变更不可直接操作、必须走 mutation / action”的约束。我们把这个约束加上,完成一个更像“框架”的迷你库。

function createStore({ state, mutations = {}, actions = {} }) {
  const store = reactive(state)
  const commit = (type, payload) => {
    const mutation = mutations[type]
    if (mutation) mutation(store, payload)
    else console.warn(`Unknown mutation: ${type}`)
  }
  const dispatch = (type, payload) => {
    const action = actions[type]
    if (action) return action({ commit, state: store }, payload)
    else console.warn(`Unknown action: ${type}`)
  }
  return { state: store, commit, dispatch }
}

用起来是不是有点熟悉了?

const store = createStore({
  state: { count: 0 },
  mutations: {
    increment(state) { state.count++ }
  },
  actions: {
    async incrementAsync(ctx) {
      await new Promise(r => setTimeout(r, 1000))
      ctx.commit('increment')
    }
  }
})

effect(() => {
  document.querySelector('#count').textContent = store.state.count
})
store.commit('increment')        // UI 立即更新
store.dispatch('incrementAsync') // 1 秒后更新

component 里只需要引入 `store.state` 和 `store.dispatch`,视图就自动跟随数据变化。这就是响应式状态管理的全部魔法。

理解精髓,更要懂取舍

手写这个库,不是为了挑战 Vue / MobX,而是为了看清它们背后的本质。你还会发现一个有趣的点:Vue 3 的 `computed` 不过是 `effect` 的返回值缓存,`watch` 不过是 `effect` 加一个“比对旧值”的逻辑。当你能亲手搭出这个小小的“积木塔”,再看框架源码,就不会觉得是在看天书了。

响应式的门槛不在于高阶语法,而在于“状态变更如何高效驱动视图”这一模型转换。从订阅发布到依赖收集,从 Proxy 到批量调度,每一步都蕴含着设计决策。真正理解了这些,再去选择使用何种框架,心中的底气都是不一样的。

试着亲手写一遍吧,然后把代码贴到评论里,我们聊聊哪一步最让你豁然开朗。

全部回复 0

还没有回复,来抢沙发~