发布订阅模式解耦状态变更与响应逻辑,避免全局变量导致的UI不同步;EventEmitter是轻量实现基底,需注意事件命名隔离、错误捕获及组件卸载时手动取消订阅。

状态管理为什么需要发布订阅模式
直接用全局变量或 Object 存状态,改一处、忘了通知依赖方,UI 就不同步。发布订阅(Pub/Sub)把“谁改了状态”和“谁要响应”解耦——状态变更只负责 publish,组件只管 subscribe,中间靠事件中心转发。
EventEmitter 是最轻量的实现基底
浏览器原生没有 EventEmitter,但 Node.js 有;前端自己写一个 20 行内就能跑起来,比引入全家桶更可控。
class EventEmitter {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data));
}
}
unsubscribe(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
-
subscribe和publish不要求先后顺序:先 publish 后 subscribe 会丢消息,这是设计使然,不是 bug - 如果需要“历史状态回放”,得额外加
lastValue缓存,比如 RxJS 的BehaviorSubject - 多个同名
event共享一个回调队列,别误以为是“每个实例独立”
在 React 中用 useEffect + subscribe 绑定组件生命周期
手动 unsubscribe 很容易漏,尤其组件卸载后回调还在执行,会报 Cannot update a component while rendering。
- 必须在
useEffect清理函数里调用unsubscribe - 避免闭包捕获过期的 state,用
ref或setState函数式更新 - 不要在
publish里传大对象,深拷贝开销大;推荐传id或diff字段
function Counter() {
const [count, setCount] = useState(0);
const eventBus = useRef(new EventEmitter()).current;
useEffect(() => {
const handler = (data) => setCount(prev => prev + data.delta);
eventBus.subscribe('counter:update', handler);
return () => eventBus.unsubscribe('counter:update', handler);
}, []);
const handleClick = () => eventBus.publish('counter:update', { delta: 1 });
return ;
}
和 Redux / Zustand 的关键区别在哪
发布订阅本身不管理状态,只做通信;Redux 把状态树、reducer、dispatch 都收束到一起,Zustand 则用函数式 API 封装了 subscribe + setState 的组合。自己写 Pub/Sub 时最容易忽略的是:
立即学习“Java免费学习笔记(深入)”;
- 没做事件命名空间隔离,
'user:login'和'cart:login'冲突了 - 忘记对
callback做try/catch,一个组件报错导致整个事件流中断 - 用字符串拼接生成
event名,比如`item:${id}:update`,但id为空或含特殊字符就失效
真要长期维护,建议从 EventEmitter 起手,但上线前补上错误边界和命名校验——这比一开始选框架更暴露问题本质。










