React中Socket.IO组件连接泄漏问题排查与复现咨询
连接泄漏的核心原因
从你的代码中可以定位到几个关键问题点:
组件卸载时Socket清理不彻底
你在空依赖的useEffect返回函数里仅调用了socketRef.current?.disconnect(),但Socket.IO默认开启自动重连(除非配置reconnection: false)。登出时组件卸载后,旧Socket实例未被彻底销毁,会持续尝试重连;同时新登录操作会创建新Socket,最终导致连接堆积。另外,你未将socketRef.current置为null,旧实例的引用始终存在,无法被GC回收。initializeSocket并发调用未做限制initializeSocket是异步函数,当socketNeedsRestart短时间内多次触发(比如快速切换页面焦点、频繁执行登出登录),会导致多个initializeSocket同时执行。前一个还未完成旧Socket的断开与实例替换,后一个已经开始创建新Socket,旧Socket可能仍处于连接状态,最终造成重复连接。事件监听未完全清理
第二个useEffect的返回函数只移除了Connect、ConnectionError、Disconnect事件的监听,但UsersOnline事件的监听未被移除。当旧Socket实例未被彻底销毁时,它依然会触发UsersOnline的回调,同时新Socket也在监听,不仅导致逻辑混乱,还会阻碍旧实例被回收。useEffect依赖缺失
第一个useEffect依赖socketNeedsRestart,但socketNeedsRestart的计算依赖isFocused和isLoadingSocket,你未将isFocused加入依赖数组,导致isFocused变化时,这个useEffect不会重新执行,可能出现socketNeedsRestart状态判断失效、重复触发初始化的情况。
复现大量创建连接的场景
可以通过以下操作复现问题:
- 快速重复登出登录:短时间内连续执行登出、登录操作,每次登录都会挂载
Socket组件并触发initializeSocket。由于异步函数的执行延迟,旧Socket还未完成断开,新Socket已创建,同时旧Socket的重连机制仍在运行,导致多个连接同时存在。 - 页面焦点快速切换+登录:登录状态下快速切换浏览器窗口焦点(触发
isFocused变化),同时执行登出登录,会多次触发socketNeedsRestart,导致initializeSocket被并发调用,创建多个Socket实例。 getToken延迟场景:如果getToken因网络或其他原因延迟返回,此时多次触发socketNeedsRestart,会导致多个initializeSocket进入等待token的状态,后续会同时创建Socket连接。
修复方案
针对上述问题,逐一修改代码:
彻底清理Socket实例
组件卸载时,不仅断开连接,还要关闭重连机制并清空引用:useEffect(() => { return () => { if (socketRef.current) { // 断开连接并禁止自动重连 socketRef.current.disconnect(true); // 移除所有事件监听 socketRef.current.removeAllListeners(); // 清空引用,允许GC回收 socketRef.current = null; } }; }, []);限制
initializeSocket并发执行
添加isInitializing的ref,防止函数被并发调用:const isInitializing = useRef(false); async function initializeSocket() { if (isInitializing.current) return; isInitializing.current = true; try { const token = await getToken(); setIsLoadingSocket(true); // 彻底清理旧Socket if (socketRef.current) { socketRef.current.disconnect(true); socketRef.current.removeAllListeners(); socketRef.current = null; } socketRef.current = io(`${SOCKET_HOST}`, { secure: true, reconnectionDelay: 5000, transports: ['websocket', 'polling'], path: ENVIRONMENT !== Environment.local ? '/api/socket.io/' : '', auth: { token }, }); console.log(`socket initialized`); } finally { isInitializing.current = false; } }修复useEffect依赖
将isFocused加入第一个useEffect的依赖数组,移除eslint禁用注释:useEffect(() => { if (socketNeedsRestart) { initializeSocket(); } }, [socketNeedsRestart, isFocused]);完整清理事件监听
在替换Socket实例或组件卸载时,移除所有绑定的事件监听,包括UsersOnline:useEffect(() => { const socket = socketRef.current; if (!socket) return; socket.on(SocketLifecycleEvent.Connect, () => { console.log('socket connected'); setIsLoadingSocket(false); }); socket.on(SocketMessage.UsersOnline, (message) => { updateOnlineUsers(message.onlineUserIDs); }); return () => { socket.off(SocketLifecycleEvent.Connect); socket.off(SocketMessage.UsersOnline); socket.off(SocketLifecycleEvent.ConnectionError); socket.off(SocketLifecycleEvent.Disconnect); }; }, [isLoadingSocket]);登出时主动清理
如果应用有全局用户状态(如Context或Redux),可监听用户登出动作,主动清理Socket:const { isLoggedIn } = useAuthContext(); useEffect(() => { if (!isLoggedIn && socketRef.current) { socketRef.current.disconnect(true); socketRef.current.removeAllListeners(); socketRef.current = null; } }, [isLoggedIn]);
内容的提问来源于stack exchange,提问作者Nimrod Sadeh

