在中小型前端项目里,引入庞大的状态管理框架往往带来不必要的体积和复杂度。通过原生JavaScript的发布订阅模式,我们可以自己实现一个无依赖、轻量且易扩展的状态管理库,核心代码甚至可以控制在百行以内。它只做三件事:集中存储状态、统一修改入口、通知订阅者。下面我们一步步拆开讲。

一、核心原理:发布订阅模式
状态管理库的本质是一个中心化的数据存储,配合一个消息机制。当数据发生变化时,所有关心这部分数据的视图或逻辑都能收到通知并作出响应。这种机制在JavaScript里最基础的实现就是发布订阅(Publish-Subscribe)。
我们维护一个监听器列表,对外暴露subscribe方法用于注册回调,暴露setState方法用于修改状态并触发回调。为了避免外部直接篡改内部状态,状态对象应该通过闭包保护起来,只提供只读的getState。
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
function getState() {
return state;
}
function subscribe(listener) {
listeners.add(listener);
// 返回取消订阅函数
return function unsubscribe() {
listeners.delete(listener);
};
}
function setState(partial) {
const next = typeof partial === 'function'
? partial(state)
: partial;
state = Object.assign({}, state, next);
listeners.forEach(function (fn) {
fn(state);
});
}
return { getState, subscribe, setState };
}
二、支持按需订阅与避免重复渲染
上面的基础版在状态任意字段变化时都会通知所有监听器,这在视图层容易造成重复渲染。我们可以让subscribe支持传入一个选择器函数,只在某些字段变化时才触发回调。
具体做法是订阅时保存上一次选出的片段,触发通知前用新状态计算出当前片段,通过浅比较判断是否需要调用回调。这样组件只关心自己用的数据,性能更好,也避免了无关更新。
function createStore(initialState) {
let state = initialState;
const entries = new Set();
function getState() {
return state;
}
function subscribe(selector, listener) {
let prev = selector(state);
const entry = {
selector: selector,
listener: listener,
prev: prev
};
entries.add(entry);
return function () {
entries.delete(entry);
};
}
function setState(partial) {
const next = typeof partial === 'function'
? partial(state)
: partial;
state = Object.assign({}, state, next);
entries.forEach(function (e) {
const cur = e.selector(state);
if (cur !== e.prev) {
e.prev = cur;
e.listener(cur, state);
}
});
}
return { getState, subscribe, setState };
}
三、与主流框架方案的对比
自己实现的库和Redux相比,缺少中间件、时间旅行调试等高级特性,但优势是零依赖、体积小、概念少。在不需要复杂副作用管理的后台系统、嵌入式页面中,这种轻量方案反而更容易维护。
和React的useState比,我们的库是跨组件共享的全局 store,不需要逐层传递 props。如果配合极少量的桥接代码,就能在任意框架中使用,不绑定技术栈。
| 方案 | 依赖体积 | 学习成本 | 适用场景 |
|---|---|---|---|
| Redux | 约 2KB 以上加中间件 | 高 | 大型复杂应用 |
| 自研轻量库 | 小于 1KB | 低 | 中小项目、嵌入式 |
| 框架内置 State | 随框架 | 中 | 单组件或局部 |
四、内存泄漏与最佳实践
使用发布订阅最容易犯的错误是组件卸载后忘记取消订阅,导致监听器越积越多。因此subscribe必须返回取消函数,并在组件销毁生命周期里调用。上面的实现中我们已经返回了移除函数。
另一个实践是禁止外部直接修改getState拿到的对象。虽然我们返回的是同一引用,但良好的约定是所有变更走setState,这样变更可追踪。若更严格,可在getState里返回浅拷贝,不过会带来轻微性能损耗,可按需取舍。
// 在组件中使用示例
const store = createStore({ count: 0, name: 'test' });
const unsub = store.subscribe(
function (s) { return s.count; },
function (count) {
console.log('count changed:', count);
}
);
store.setState({ count: 1 });
unsub(); // 组件卸载时调用
五、完整最小实现
把前面的能力合并,就得到了一个无依赖的状态管理库。它支持全局状态、按需订阅、安全取消,可直接放进项目里使用,也可以继续扩展出中间件、批量更新等能力。
以下代码可直接保存为store.js引用,没有任何外部包依赖,适合作为你理解状态管理底层机制的起点。
function createStore(initialState) {
let state = initialState;
const entries = new Set();
function getState() {
return state;
}
function subscribe(selector, listener) {
let prev = selector(state);
const entry = { selector: selector, listener: listener, prev: prev };
entries.add(entry);
return function () {
entries.delete(entry);
};
}
function setState(partial) {
const next = typeof partial === 'function' ? partial(state) : partial;
state = Object.assign({}, state, next);
entries.forEach(function (e) {
const cur = e.selector(state);
if (cur !== e.prev) {
e.prev = cur;
e.listener(cur, state);
}
});
}
return { getState, subscribe, setState };
}
// 使用示例
const store = createStore({ count: 0 });
const unsub = store.subscribe(
function (s) { return s.count; },
function (c) { console.log(c); }
);
store.setState({ count: 5 });
unsub();
JavaScriptstate_managementpublish_subscribe修改时间:2026-08-03 21:06:15