手写一个前端状态管理库,深入理解响应式原理
当面试官让我手写状态管理库
前端框架千千万,状态管理却总是那几招: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 到批量调度,每一步都蕴含着设计决策。真正理解了这些,再去选择使用何种框架,心中的底气都是不一样的。
试着亲手写一遍吧,然后把代码贴到评论里,我们聊聊哪一步最让你豁然开朗。
管理员
黑卡会员