单例模式在SPA中易致内存累积,关键在于单例是否在不该驻留时仍持有活跃引用:需检查闭包集合变量是否只增不减、事件监听器是否配对移除、连接类单例是否重复建立未关闭、HMR是否导致实例重建、全局状态是否无节制嵌套或未冻结。

单例模式在单页应用中一旦设计不当,容易造成内存持续累积,尤其在长生命周期对象(如 WebSocket 管理器、事件总线、全局状态缓存)上表现明显。识别这类问题,关键不在于“有没有单例”,而在于“单例是否在不该驻留时仍持有活跃引用”。
看闭包内变量是否随路由切换持续增长
SPA 中频繁切换路由时,若单例内部缓存了组件实例、回调函数或 DOM 引用,而未及时清理,就会形成内存堆积。
- 检查闭包中是否存在类似 handlers = []、subscribers = new Map() 的集合类变量
- 确认每次页面跳转后,这些集合的 size 是否只增不减(可用 console.log(handlers.length) 在关键路径打点验证)
- 特别注意:使用 addEventListener 或 uni.onXXX 订阅全局事件时,若没配对调用 removeEventListener 或 uni.offXXX,监听器会持续挂载在单例上
查连接类单例是否重复建立却未关闭旧连接
WebSocket、SSE、长轮询等通道类单例,最容易因状态管理缺失导致“假单例、真多连”。
- 在 onOpen 回调里打印日志,观察同一会话中是否出现多次 “connected” 输出
- 用浏览器开发者工具的 Network → WS 标签页,查看当前活跃的 WebSocket 连接数是否 >1
- 检查 destroy() 方法是否被调用 —— 尤其在组件 onUnload 或 beforeUnmount 阶段,若从未触发,旧连接就一直占着内存和端口
验模块热更新后单例是否意外重建
Vite 或 Webpack HMR 在开发阶段重载模块时,会销毁并重建闭包,导致原单例失效,新单例又悄悄创建连接或缓存,老实例却未释放。
- 修改一个 .vue 或 .ts 文件保存,观察控制台是否重复打印初始化日志(如 “WebSocket instance created”)
- 在闭包顶层加 console.count('singleton module loaded'),热更后计数是否递增
- 若计数增加,说明闭包已重建,此前所有闭包内变量(包括 WebSocket 实例、定时器 ID、事件监听器)都变成孤立对象,但可能仍被底层 API 持有引用,无法被 GC
盯全局状态对象是否无节制嵌套或未冻结
有些单例会把整个应用状态树挂载为自身属性,若状态结构深度嵌套、含大量未序列化的函数或 DOM 节点,也会拖慢 GC 效率。
- 用 Performance → Memory 录制一段操作,查看堆快照中是否有大量重复的 Object 或 Closure 实例
- 检查单例中存储的状态是否用了 JSON.parse(JSON.stringify(...)) 做深拷贝 —— 这种操作在高频更新场景下极易引发内存抖动
- 确认复杂状态对象是否调用 Object.freeze() 或使用 Immutable.js 控制不可变性,避免 V8 对可变大对象做过度优化跟踪

















