单例应确保全局唯一且延迟初始化,推荐模块作用域+闭包封装;观察者需防内存泄漏、用常量管理事件名;单例+观察者组合时须注意初始化时机、生命周期绑定与异步通知。
单例的核心是「全局唯一实例 + 延迟初始化」,不是「只写一个对象字面量」。直接 const instance = { ... } 看似简单,但无法控制构造逻辑、无法延迟加载、无法防止多次 new,也不支持依赖注入。
推荐用模块作用域 + 闭包封装,兼顾私有状态与可测试性:
const Singleton = (function() {
let instance = null;
function createInstance() {
return {
data: [],
add(item) { this.data.push(item); },
getCount() { return this.data.length; }
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
注意点:
static instance + 构造器判空——ES6 class 无私有构造器,new Singleton() 仍可绕过控制原生 EventTarget(浏览器)和 events.EventEmitter(Node.js)是观察者模式的成熟实现,但它们不解决「谁负责订阅/取消订阅」「事件名拼错静默失败」「监听器内存泄漏」这些实际问题。
手写轻量版更可控,尤其适合组件通信或状态管理内部使用:
class Observer {
constructor() {
this._callbacks = new Map();
}
subscribe(event, fn) {
if (!this._callbacks.has(event)) {
this._callbacks.set(event, new Set());
}
this._callbacks.get(event).add(fn);
}
unsubscribe(event, fn) {
const fns = this._callbacks.get(event);
if (fns) fns.delete(fn);
}
notify(event, ...args) {
const fns = this._callbacks.get(event);
if (fns) fns.forEach(fn => fn(...args));
}
}
关键细节:
Set 而非数组存监听器,避免重复 subscribe 导致多次触发unsubscribe,否则 Vue/React 组件卸载时监听器残留会引发内存泄漏user:update:profile),改用常量对象管理:const EVENTS = { USER_UPDATE: 'user:update' },避免拼写错误难排查很多项目用「单例 Store + 内置 EventEmitter」做状态管理,但容易忽略两个边界:
new Store(),但依赖的 API 客户端尚未配置完成,导致初始化失败且无重试机制useEffect(() => { store.subscribe('data', handler) }, []),但忘记在 cleanup 中 unsubscribe,handler 会持续持有旧组件闭包,造成内存泄漏和重复渲染notify() 是同步的,如果某个监听器执行慢或报错,会阻塞后续监听器;需考虑加 setTimeout(() => fn(), 0) 或用 queueMicrotask 转为异步(但会丢失调用栈上下文)是否引入 rxjs、mitt 或 eventemitter3,取决于三个硬指标:
mitt 不支持,rxjs 的 Subscription 支持)on('user:*')),eventemitter3 支持,原生 EventTarget 不支持mitt 只有 200B,而 rxjs 默认打包进几 KB,若仅需 emit/listen,手写 20 行足够真正难的不是模式本身,而是谁持有订阅关系、什么时候清理、错误是否透出、事件是否可追溯——这些不会因为用了设计模式就自动解决。