如何用JavaScript实现状态管理_发布订阅模式如何工作

发布订阅模式解耦状态变更与响应逻辑,避免全局变量导致的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);
    }
  }
}
  • subscribepublish 不要求先后顺序:先 publish 后 subscribe 会丢消息,这是设计使然,不是 bug
  • 如果需要“历史状态回放”,得额外加 lastValue 缓存,比如 RxJS 的 BehaviorSubject
  • 多个同名 event 共享一个回调队列,别误以为是“每个实例独立”

在 React 中用 useEffect + subscribe 绑定组件生命周期

手动 unsubscribe 很容易漏,尤其组件卸载后回调还在执行,会报 Cannot update a component while rendering

  • 必须在 useEffect 清理函数里调用 unsubscribe
  • 避免闭包捕获过期的 state,用 refsetState 函数式更新
  • 不要在 publish 里传大对象,深拷贝开销大;推荐传 iddiff 字段
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 时最容易忽略的是:

  • 没做事件命名空间隔离,'user:login''cart:login' 冲突了
  • 忘记对 callbacktry/catch,一个组件报错导致整个事件流中断
  • 用字符串拼接生成 event 名,比如 `item:${id}:update`,但 id 为空或含特殊字符就失效

真要长期维护,建议从 EventEmitter 起手,但上线前补上错误边界和命名校验——这比一开始选框架更暴露问题本质。