结论:Vue3中不封装全局单例且不清理组件卸载资源,90%概率内存泄漏或重复连接;仅调用close()不够,因未清除onmessage等回调闭包引用、未清理心跳定时器、未避免多实例并发,须用useWebSocket composable统一管理连接生命周期并显式置空引用。

直接说结论:Vue3里用原生WebSocket做高并发实时更新,不封装成全局单例+不处理组件卸载时的连接清理,90%概率出现内存泄漏或重复连接。
为什么onUnmounted里只调用close()还不够
很多开发者以为组件销毁时执行socket.value.close()就万事大吉,但实际漏掉了三个关键点:
-
onmessage、onerror、onclose这些回调函数如果绑在全局对象(比如window)或未清除的闭包里,会持续持有对组件响应式数据的引用 - 如果用了
setInterval做心跳检测,onUnmounted没clearInterval,定时器照常运行,还会不断触发send()并尝试访问已销毁的ref - 多个组件同时调用
new WebSocket()但没共享实例,后端看到的是多个独立连接,而前端状态却可能互相干扰(比如一个组件关了,另一个还在发消息)
如何安全地复用同一个WebSocket连接
核心是把连接生命周期从组件提升到应用层,用一个可复用的useWebSocket composable 管理:
- 内部用
let instance: WebSocket | null = null缓存单例,首次调用时创建,后续直接返回 - 暴露
connect()和disconnect()方法,由上层决定何时启停,而不是依赖组件挂载/卸载钩子 - 所有事件监听都绑定在
instance上,且在disconnect()中显式设为null并移除监听器(不要只靠close()) - 如果需要跨组件广播消息,用
const emitter = new EventEmitter()或createApp().config.globalProperties.$bus,避免直接传递socket引用
示例关键逻辑:
递归分析 Vue 项目组件依赖,从入口文件生成组件层级图,支持 Vue 2/3,输出组件名、文件路径和属性。适用于分析组件结构、排查依赖或了解项目架构。
立即学习“前端免费学习笔记(深入)”;
function useWebSocket(url: string) {
let socket: WebSocket | null = null
const connect = () => {
if (socket && socket.readyState === WebSocket.OPEN) return
socket = new WebSocket(url)
socket.onmessage = (e) => emitter.emit('message', e.data)
socket.onclose = () => emitter.emit('close')
}
const disconnect = () => {
if (socket) {
socket.close()
socket = null // 必须置空,否则下次connect不会新建
}
}
return { connect, disconnect }
}
readyState判断不能只看OPEN,还要防CONNECTING竞争
高频发送场景下,socket.send()抛出InvalidStateError: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state很常见。这不是bug,是真实网络延迟导致的状态滞后。
- 不要在
connect()后立刻send(),得等onopen回调触发后再允许发送 - 发送前加一层状态检查:
if (socket?.readyState === WebSocket.OPEN),但要注意这个判断不是原子的——检查完瞬间可能断连 - 更稳妥的做法是加发送队列 + 状态锁:
let isSending = false,onopen后才允许isSending = true,失败时进重试队列而非直接报错 - 服务端也要配合做连接保活,避免Nginx等代理在60秒无流量后静默断连
高并发下消息堆积和渲染卡顿怎么破
当每秒收到几十条消息(比如行情推送),ref频繁赋值会触发大量响应式更新,UI线程直接卡住。
- 用
shallowRef替代ref存原始消息数组,避免对每条消息做深层响应式追踪 - 批量更新时合并操作:收集100ms内的消息,一次性
push到数组,再用nextTick触发渲染 - 对非关键字段(如时间戳、ID)做
Object.freeze(),防止Proxy递归劫持 - 真正需要响应式的只有“是否已读”“是否高亮”这类UI状态,其他纯数据走
markRaw
最后提醒一句:WebSocket不是万能胶水。如果业务只需要“最终一致性”,比如库存数字,用Server-Sent Events(SSE)更轻量;只有双向、低延迟、需客户端主动发指令的场景(聊天、协同编辑),才值得投入精力做这套封装。别为了技术而技术。


















