You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中正确使用socket.on:爬虫项目Socket事件监听异常问题

解决React中Socket.io事件逐个监听的问题

我来帮你分析下问题所在,然后给出针对性的修复方案:

问题根源

  1. Socket实例重复创建:你在组件顶层直接写socket = io(ENDPOINT),这会导致组件每次重新渲染时都生成新的Socket连接,之前绑定的事件监听会直接失效。
  2. 闭包导致状态更新异常:useEffect的依赖是空数组,里面引用的crawlingBlog始终是组件初始化时的空数组,每次concat操作都是基于旧值,无法正确累积新收到的博客数据。
  3. 缺少监听清理逻辑:组件卸载时没有移除Socket监听、断开连接,可能引发内存泄漏问题。

修复后的完整代码

import { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client';

const App = () => {
  const [crawlingBlog, setCrawlingBlog] = useState([]);
  const [crawledBlogs, setCrawledBlogs] = useState([]);
  const [timeRecords, setTimeRecords] = useState([]);
  const socketRef = useRef(null); // 用useRef保存socket实例,避免重复创建
  const ENDPOINT = 'http://localhost:4000'; // 记得加上http协议头

  useEffect(() => {
    // 初始化socket连接
    socketRef.current = io(ENDPOINT);

    // 定义各事件的处理函数
    const handleCrawling = (blog) => {
      // 用函数式更新避免闭包问题
      setCrawlingBlog(prevState => [...prevState, blog]);
    };

    const handleCrawled = (blog) => {
      setCrawledBlogs(prevState => [...prevState, blog]);
    };

    const handleTimeTaken = (data) => {
      setTimeRecords(prevState => [...prevState, data]);
    };

    // 绑定事件监听
    socketRef.current.on('crawling', handleCrawling);
    socketRef.current.on('crawled', handleCrawled);
    socketRef.current.on('timeTaken', handleTimeTaken);

    // 组件卸载时清理资源
    return () => {
      // 精准移除对应监听
      socketRef.current.off('crawling', handleCrawling);
      socketRef.current.off('crawled', handleCrawled);
      socketRef.current.off('timeTaken', handleTimeTaken);
      // 断开socket连接
      socketRef.current.disconnect();
    };
  }, []); // 空依赖确保只初始化一次连接和监听

  return (
    <div className="app-container">
      <h2>正在爬取的博客</h2>
      <ul>
        {crawlingBlog.map((blog, idx) => (
          <li key={idx}>{blog}</li>
        ))}
      </ul>

      <h2>已爬取完成的博客</h2>
      <ul>
        {crawledBlogs.map((blog, idx) => (
          <li key={idx}>{blog}</li>
        ))}
      </ul>

      <h2>爬取耗时记录</h2>
      <ul>
        {timeRecords.map((record, idx) => (
          <li key={idx}>{record.blog}: {record.time}ms</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

关键修复点说明

  • 用useRef保存Socket实例:useRef的取值在组件整个生命周期内保持稳定,不会因为组件渲染重新创建连接,确保所有监听都绑定在同一个Socket实例上。
  • 函数式状态更新:setCrawlingBlog(prev => [...prev, blog])这种写法依赖前一次的状态值,彻底避免了闭包导致的旧状态问题,能正确累积每次收到的博客数据。
  • 单独定义监听函数:把每个事件的处理逻辑单独抽成函数,这样在组件卸载时可以精准移除对应的监听(如果用匿名函数,off方法无法匹配到对应的监听)。
  • 添加清理逻辑:在useEffect的返回函数中移除监听并断开连接,防止组件卸载后仍有无效的监听占用资源。
  • 补充协议头:Socket连接地址要加上http://(或https://),否则部分环境下会出现连接失败的情况。

这样修改后,你的React组件就能正确逐个监听服务端发送的所有事件,状态也会实时同步更新。

内容的提问来源于stack exchange,提问作者Aayush Agrawal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 08:32:37